use of net.pms.configuration.RendererConfiguration in project UniversalMediaServer by UniversalMediaServer.
the class RequestHandler method run.
@Override
public void run() {
Request request = null;
StartStopListenerDelegate startStopListenerDelegate = new StartStopListenerDelegate(socket.getInetAddress().getHostAddress());
try {
int receivedContentLength = -1;
String userAgentString = null;
ArrayList<String> identifiers = new ArrayList<>();
RendererConfiguration renderer = null;
InetSocketAddress remoteAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
InetAddress ia = remoteAddress.getAddress();
boolean isSelf = ia.getHostAddress().equals(PMS.get().getServer().getHost());
// Apply the IP filter
if (filterIp(ia)) {
throw new IOException("Access denied for address " + ia + " based on IP filter");
}
// The handler makes a couple of attempts to recognize a renderer from its requests.
// IP address matches from previous requests are preferred, when that fails request
// header matches are attempted and if those fail as well we're stuck with the
// default renderer.
// Attempt 1: try to recognize the renderer by its socket address from previous requests
renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(ia);
// If the renderer exists but isn't marked as loaded it means it's unrecognized
// by upnp and we still need to attempt http recognition here.
boolean unrecognized = renderer == null || !renderer.loaded;
RendererConfiguration.SortedHeaderMap sortedHeaders = unrecognized ? new RendererConfiguration.SortedHeaderMap() : null;
// Gather all the headers
ArrayList<String> headerLines = new ArrayList<>();
String line = br.readLine();
while (line != null && line.length() > 0) {
headerLines.add(line);
if (sortedHeaders != null) {
sortedHeaders.put(line);
}
line = br.readLine();
}
if (unrecognized) {
// Attempt 2: try to recognize the renderer by matching headers
renderer = RendererConfiguration.getRendererConfigurationByHeaders(sortedHeaders, ia);
}
for (String headerLine : headerLines) {
// The request object is created inside the while loop.
if (request != null && request.getMediaRenderer() == null && renderer != null) {
request.setMediaRenderer(renderer);
}
if (headerLine.toUpperCase().startsWith("USER-AGENT")) {
// Is the request from our own Cling service, i.e. self-originating?
if (isSelf && headerLine.contains("UMS/")) {
// LOGGER.trace("Ignoring self-originating request from {}:{}", ia, remoteAddress.getPort());
return;
}
userAgentString = headerLine.substring(headerLine.indexOf(':') + 1).trim();
} else if (renderer != null && headerLine.startsWith("X-PANASONIC-DMP-Profile:")) {
PanasonicDmpProfiles.parsePanasonicDmpProfiles(headerLine, renderer);
}
try {
StringTokenizer s = new StringTokenizer(headerLine);
String temp = s.nextToken();
if (temp.equals("SUBSCRIBE") || temp.equals("GET") || temp.equals("POST") || temp.equals("HEAD")) {
request = new Request(temp, s.nextToken().substring(1));
if (s.hasMoreTokens() && s.nextToken().equals("HTTP/1.0")) {
request.setHttp10(true);
}
} else if (request != null && temp.toUpperCase().equals("CALLBACK:")) {
request.setSoapaction(s.nextToken());
} else if (request != null && temp.toUpperCase().equals("SOAPACTION:")) {
request.setSoapaction(s.nextToken());
} else if (headerLine.toUpperCase().contains("CONTENT-LENGTH:")) {
receivedContentLength = Integer.parseInt(headerLine.substring(headerLine.toUpperCase().indexOf("CONTENT-LENGTH: ") + 16));
} else if (headerLine.toUpperCase().contains("RANGE: BYTES=")) {
String nums = headerLine.substring(headerLine.toUpperCase().indexOf("RANGE: BYTES=") + 13).trim();
StringTokenizer st = new StringTokenizer(nums, "-");
if (!nums.startsWith("-")) {
request.setLowRange(Long.parseLong(st.nextToken()));
}
if (!nums.startsWith("-") && !nums.endsWith("-")) {
request.setHighRange(Long.parseLong(st.nextToken()));
} else {
request.setHighRange(-1);
}
} else if (headerLine.toLowerCase().contains("transfermode.dlna.org:")) {
request.setTransferMode(headerLine.substring(headerLine.toLowerCase().indexOf("transfermode.dlna.org:") + 22).trim());
} else if (headerLine.toLowerCase().contains("getcontentfeatures.dlna.org:")) {
request.setContentFeatures(headerLine.substring(headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") + 28).trim());
} else if (headerLine.toUpperCase().contains("TIMESEEKRANGE.DLNA.ORG: NPT=")) {
// firmware 2.50+
String timeseek = headerLine.substring(headerLine.toUpperCase().indexOf("TIMESEEKRANGE.DLNA.ORG: NPT=") + 28);
if (timeseek.endsWith("-")) {
timeseek = timeseek.substring(0, timeseek.length() - 1);
} else if (timeseek.indexOf('-') > -1) {
timeseek = timeseek.substring(0, timeseek.indexOf('-'));
}
request.setTimeseek(convertStringToTime(timeseek));
} else if (headerLine.toUpperCase().contains("TIMESEEKRANGE.DLNA.ORG : NPT=")) {
// firmware 2.40
String timeseek = headerLine.substring(headerLine.toUpperCase().indexOf("TIMESEEKRANGE.DLNA.ORG : NPT=") + 29);
if (timeseek.endsWith("-")) {
timeseek = timeseek.substring(0, timeseek.length() - 1);
} else if (timeseek.indexOf('-') > -1) {
timeseek = timeseek.substring(0, timeseek.indexOf('-'));
}
request.setTimeseek(convertStringToTime(timeseek));
} else {
/*
* If we made it to here, none of the previous header checks matched.
* Unknown headers make interesting logging info when we cannot recognize
* the media renderer, so keep track of the truly unknown ones.
*/
boolean isKnown = false;
// Try to match possible known headers.
String lowerCaseHeaderLine = headerLine.toLowerCase();
for (String knownHeaderString : KNOWN_HEADERS) {
if (lowerCaseHeaderLine.startsWith(knownHeaderString.toLowerCase())) {
isKnown = true;
break;
}
}
// It may be unusual but already known
if (renderer != null) {
String additionalHeader = renderer.getUserAgentAdditionalHttpHeader();
if (StringUtils.isNotBlank(additionalHeader) && lowerCaseHeaderLine.startsWith(additionalHeader)) {
isKnown = true;
}
}
if (!isKnown) {
// Truly unknown header, therefore interesting. Save for later use.
identifiers.add(headerLine);
}
}
} catch (IllegalArgumentException e) {
LOGGER.error("Error parsing HTTP headers: {}", e.getMessage());
LOGGER.trace("", e);
}
}
if (request != null) {
// Still no media renderer recognized?
if (renderer == null) {
// Attempt 3: Not really an attempt; all other attempts to recognize
// the renderer have failed. The only option left is to assume the
// default renderer.
renderer = RendererConfiguration.resolve(ia, null);
request.setMediaRenderer(renderer);
if (renderer != null) {
LOGGER.debug("Using default media renderer \"{}\"", renderer.getConfName());
if (userAgentString != null && !userAgentString.equals("FDSSDP")) {
// We have found an unknown renderer
identifiers.add(0, "User-Agent: " + userAgentString);
renderer.setIdentifiers(identifiers);
LOGGER.info("Media renderer was not recognized. Possible identifying HTTP headers:\n{}", StringUtils.join(identifiers, "\n"));
}
} else {
// it means we know via upnp that it's not really a renderer.
return;
}
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Recognized media renderer \"{}\"", renderer.getRendererName());
}
}
if (receivedContentLength > 0) {
char[] buf = new char[receivedContentLength];
br.read(buf);
if (request != null) {
String textContent = new String(buf);
request.setTextContent(textContent);
if (LOGGER.isTraceEnabled()) {
logMessageReceived(headerLines, textContent, socket.getRemoteSocketAddress(), renderer);
}
} else if (LOGGER.isTraceEnabled()) {
logMessageReceived(headerLines, null, socket.getRemoteSocketAddress(), renderer);
}
}
if (request != null) {
request.answer(output, startStopListenerDelegate);
}
if (request != null && request.getInputStream() != null) {
request.getInputStream().close();
}
} catch (IOException e) {
LOGGER.error("Unexpected IO error in {}: {}", getClass().getName(), e.getMessage());
// LOGGER.trace("", e);
if (request != null && request.getInputStream() != null) {
try {
LOGGER.trace("Closing input stream: {}", request.getInputStream());
request.getInputStream().close();
} catch (IOException e1) {
LOGGER.error("Error closing input stream: {}", e1);
LOGGER.trace("", e1);
}
}
} finally {
try {
output.close();
br.close();
socket.close();
} catch (IOException e) {
LOGGER.error("Error closing connection: {}", e.getMessage());
LOGGER.trace("", e);
}
startStopListenerDelegate.stop();
}
}
use of net.pms.configuration.RendererConfiguration in project UniversalMediaServer by UniversalMediaServer.
the class RequestHandlerV2 method messageReceived.
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) throws Exception {
RequestV2 request = null;
RendererConfiguration renderer = null;
String userAgentString = null;
ArrayList<String> identifiers = new ArrayList<>();
HttpRequest nettyRequest = this.nettyRequest = (HttpRequest) event.getMessage();
InetSocketAddress remoteAddress = (InetSocketAddress) event.getChannel().getRemoteAddress();
InetAddress ia = remoteAddress.getAddress();
// Is the request from our own Cling service, i.e. self-originating?
boolean isSelf = ia.getHostAddress().equals(PMS.get().getServer().getHost()) && nettyRequest.headers().get(HttpHeaders.Names.USER_AGENT) != null && nettyRequest.headers().get(HttpHeaders.Names.USER_AGENT).contains("UMS/");
// Filter if required
if (isSelf || filterIp(ia)) {
event.getChannel().close();
/*if (isSelf && LOGGER.isTraceEnabled()) {
LOGGER.trace("Ignoring self-originating request from {}:{}", ia, remoteAddress.getPort());
}*/
return;
}
request = new RequestV2(nettyRequest.getMethod().getName(), nettyRequest.getUri().substring(1));
if (nettyRequest.getProtocolVersion().getMinorVersion() == 0) {
request.setHttp10(true);
}
HttpHeaders headers = nettyRequest.headers();
// The handler makes a couple of attempts to recognize a renderer from its requests.
// IP address matches from previous requests are preferred, when that fails request
// header matches are attempted and if those fail as well we're stuck with the
// default renderer.
// Attempt 1: try to recognize the renderer by its socket address from previous requests
renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(ia);
// by upnp and we still need to attempt http recognition here.
if (renderer == null || !renderer.loaded) {
// Attempt 2: try to recognize the renderer by matching headers
renderer = RendererConfiguration.getRendererConfigurationByHeaders(headers.entries(), ia);
}
if (renderer != null) {
request.setMediaRenderer(renderer);
}
Set<String> headerNames = headers.names();
Iterator<String> iterator = headerNames.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
String headerLine = name + ": " + headers.get(name);
if (headerLine.toUpperCase().startsWith("USER-AGENT")) {
userAgentString = headerLine.substring(headerLine.indexOf(':') + 1).trim();
} else if (renderer != null && name.equals("X-PANASONIC-DMP-Profile")) {
PanasonicDmpProfiles.parsePanasonicDmpProfiles(headers.get(name), renderer);
}
try {
StringTokenizer s = new StringTokenizer(headerLine);
String temp = s.nextToken();
if (temp.toUpperCase().equals("SOAPACTION:")) {
request.setSoapaction(s.nextToken());
} else if (temp.toUpperCase().equals("CALLBACK:")) {
request.setSoapaction(s.nextToken());
} else if (headerLine.toUpperCase().contains("RANGE: BYTES=")) {
String nums = headerLine.substring(headerLine.toUpperCase().indexOf("RANGE: BYTES=") + 13).trim();
StringTokenizer st = new StringTokenizer(nums, "-");
if (!nums.startsWith("-")) {
request.setLowRange(Long.parseLong(st.nextToken()));
}
if (!nums.startsWith("-") && !nums.endsWith("-")) {
request.setHighRange(Long.parseLong(st.nextToken()));
} else {
request.setHighRange(-1);
}
} else if (headerLine.toLowerCase().contains("transfermode.dlna.org:")) {
request.setTransferMode(headerLine.substring(headerLine.toLowerCase().indexOf("transfermode.dlna.org:") + 22).trim());
} else if (headerLine.toLowerCase().contains("getcontentfeatures.dlna.org:")) {
request.setContentFeatures(headerLine.substring(headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") + 28).trim());
} else {
Matcher matcher = TIMERANGE_PATTERN.matcher(headerLine);
if (matcher.find()) {
String first = matcher.group(1);
if (first != null) {
request.setTimeRangeStartString(first);
}
String end = matcher.group(2);
if (end != null) {
request.setTimeRangeEndString(end);
}
} else {
/**
* If we made it to here, none of the previous header checks matched.
* Unknown headers make interesting logging info when we cannot recognize
* the media renderer, so keep track of the truly unknown ones.
*/
boolean isKnown = false;
// Try to match known headers.
String lowerCaseHeaderLine = headerLine.toLowerCase();
for (String knownHeaderString : KNOWN_HEADERS) {
if (lowerCaseHeaderLine.startsWith(knownHeaderString)) {
isKnown = true;
break;
}
}
// It may be unusual but already known
if (!isKnown && renderer != null) {
String additionalHeader = renderer.getUserAgentAdditionalHttpHeader();
if (StringUtils.isNotBlank(additionalHeader) && lowerCaseHeaderLine.startsWith(additionalHeader)) {
isKnown = true;
}
}
if (!isKnown) {
// Truly unknown header, therefore interesting. Save for later use.
identifiers.add(headerLine);
}
}
}
} catch (Exception ee) {
LOGGER.error("Error parsing HTTP headers: {}", ee.getMessage());
LOGGER.trace("", ee);
}
}
// Still no media renderer recognized?
if (renderer == null) {
// Attempt 3: Not really an attempt; all other attempts to recognize
// the renderer have failed. The only option left is to assume the
// default renderer.
renderer = RendererConfiguration.resolve(ia, null);
request.setMediaRenderer(renderer);
if (renderer != null) {
LOGGER.debug("Using default media renderer \"{}\"", renderer.getConfName());
if (userAgentString != null && !userAgentString.equals("FDSSDP")) {
// We have found an unknown renderer
identifiers.add(0, "User-Agent: " + userAgentString);
renderer.setIdentifiers(identifiers);
LOGGER.info("Media renderer was not recognized. Possible identifying HTTP headers:\n{}", StringUtils.join(identifiers, "\n"));
PMS.get().setRendererFound(renderer);
}
} else {
// it means we know via upnp that it's not really a renderer.
return;
}
} else if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Recognized media renderer \"{}\"", renderer.getRendererName());
}
if (nettyRequest.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
byte[] data = new byte[(int) HttpHeaders.getContentLength(nettyRequest)];
ChannelBuffer content = nettyRequest.getContent();
content.readBytes(data);
String textContent = new String(data, "UTF-8");
request.setTextContent(textContent);
if (LOGGER.isTraceEnabled()) {
logMessageReceived(event, textContent, renderer);
}
} else if (LOGGER.isTraceEnabled()) {
logMessageReceived(event, null, renderer);
}
writeResponse(ctx, event, request, ia);
}
use of net.pms.configuration.RendererConfiguration in project UniversalMediaServer by UniversalMediaServer.
the class DLNAResource method stopPlaying.
/**
* Plugin implementation. When this item is going to stop playing, it will notify all the StartStopListener
* objects available.
*
* @see StartStopListener
*/
public void stopPlaying(final String rendererId, final RendererConfiguration incomingRenderer) {
final DLNAResource self = this;
final String requestId = getRequestId(rendererId);
Runnable defer = new Runnable() {
@Override
public void run() {
long start = startTime;
try {
Thread.sleep(STOP_PLAYING_DELAY);
} catch (InterruptedException e) {
LOGGER.error("stopPlaying sleep interrupted", e);
}
synchronized (requestIdToRefcount) {
final Integer refCount = requestIdToRefcount.get(requestId);
assert refCount != null;
assert refCount > 0;
requestIdToRefcount.put(requestId, refCount - 1);
if (start != startTime) {
return;
}
Runnable r = new Runnable() {
@Override
public void run() {
if (refCount == 1) {
requestIdToRefcount.put(requestId, 0);
InetAddress rendererIp;
try {
rendererIp = InetAddress.getByName(rendererId);
RendererConfiguration renderer;
if (incomingRenderer == null) {
renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(rendererIp);
} else {
renderer = incomingRenderer;
}
String rendererName = "unknown renderer";
try {
// Reset only if another item hasn't already begun playing
if (renderer.getPlayingRes() == self) {
renderer.setPlayingRes(null);
}
rendererName = renderer.getRendererName();
} catch (NullPointerException e) {
}
if (!quietPlay()) {
LOGGER.info("Stopped playing " + getName() + " on your " + rendererName);
LOGGER.debug("The full filename of which is: " + getSystemName() + " and the address of the renderer is: " + rendererId);
}
} catch (UnknownHostException ex) {
LOGGER.debug("" + ex);
}
internalStop();
for (final ExternalListener listener : ExternalFactory.getExternalListeners()) {
if (listener instanceof StartStopListener) {
// run these asynchronously for slow handlers (e.g. logging, scrobbling)
Runnable fireStartStopEvent = new Runnable() {
@Override
public void run() {
try {
((StartStopListener) listener).donePlaying(media, self);
} catch (Throwable t) {
LOGGER.error("Notification of donePlaying event failed for StartStopListener {}", listener.getClass(), t);
}
}
};
new Thread(fireStartStopEvent, "StopPlaying Event for " + listener.name()).start();
}
}
}
}
};
new Thread(r, "StopPlaying Event").start();
}
}
};
new Thread(defer, "StopPlaying Event Deferrer").start();
}
use of net.pms.configuration.RendererConfiguration in project UniversalMediaServer by UniversalMediaServer.
the class FileTranscodeVirtualFolder method resolveOnce.
/**
* This populates the file-specific transcode folder with all combinations of players,
* audio tracks and subtitles.
*/
@Override
protected void resolveOnce() {
if (getChildren().size() == 1) {
// OK
DLNAResource child = getChildren().get(0);
child.syncResolve();
RendererConfiguration renderer = null;
if (this.getParent() != null) {
renderer = this.getParent().getDefaultRenderer();
}
// create copies of the audio/subtitle track lists as we're making (local)
// modifications to them
List<DLNAMediaAudio> audioTracks = new ArrayList<>(child.getMedia().getAudioTracksList());
List<DLNAMediaSubtitle> subtitleTracks = new ArrayList<>(child.getMedia().getSubtitleTracksList());
// assemble copies for each combination of audio, subtitle and player
ArrayList<DLNAResource> entries = new ArrayList<>();
// First, add the option to simply stream the resource.
if (renderer != null) {
LOGGER.trace("Duplicating {} for direct streaming to renderer: {}", child.getName(), renderer.getRendererName());
}
DLNAResource noTranscode = createResourceWithAudioSubtitlePlayer(child, null, null, null);
addChildInternal(noTranscode);
addChapterFolder(noTranscode);
// add options for renderer capable to handle streamed subtitles
if (!configuration.isDisableSubtitles() && renderer != null && renderer.isSubtitlesStreamingSupported()) {
for (DLNAMediaSubtitle subtitle : subtitleTracks) {
// only add the option if the renderer supports the given format
if (subtitle.isExternal()) {
// do not check for embedded subs
if (renderer.isExternalSubtitlesFormatSupported(subtitle, child.getMedia())) {
DLNAResource copy = createResourceWithAudioSubtitlePlayer(child, null, subtitle, null);
copy.getMediaSubtitle().setSubsStreamable(true);
entries.add(copy);
LOGGER.trace("Duplicating {} for direct streaming subtitles {}", child.getName(), subtitle.toString());
}
}
}
}
if (audioTracks.isEmpty()) {
audioTracks.add(null);
}
if (subtitleTracks.isEmpty()) {
subtitleTracks.add(null);
} else {
// if there are subtitles, make sure a no-subtitle option is added
// for each player
DLNAMediaSubtitle noSubtitle = new DLNAMediaSubtitle();
noSubtitle.setId(-1);
subtitleTracks.add(noSubtitle);
}
for (DLNAMediaAudio audio : audioTracks) {
// Create combinations of all audio tracks, subtitles and players.
for (DLNAMediaSubtitle subtitle : subtitleTracks) {
// Create a temporary copy of the child with the audio and
// subtitle modified in order to be able to match players to it.
DLNAResource temp = createResourceWithAudioSubtitlePlayer(child, audio, subtitle, null);
// Determine which players match this audio track and subtitle
ArrayList<Player> players = PlayerFactory.getPlayers(temp);
// create a copy for each compatible player
for (Player player : players) {
DLNAResource copy = createResourceWithAudioSubtitlePlayer(child, audio, subtitle, player);
entries.add(copy);
}
}
}
// Sort the list of combinations
Collections.sort(entries, new ResourceSort(PlayerFactory.getPlayers()));
// Now add the sorted list of combinations to the folder
for (DLNAResource dlna : entries) {
LOGGER.trace("Adding {}: audio: {}, subtitle: {}, player: {}", new Object[] { dlna.getName(), dlna.getMediaAudio(), dlna.getMediaSubtitle(), (dlna.getPlayer() != null ? dlna.getPlayer().name() : null) });
addChildInternal(dlna);
addChapterFolder(dlna);
}
}
}
use of net.pms.configuration.RendererConfiguration in project UniversalMediaServer by UniversalMediaServer.
the class FFMpegVideo method getVideoFilterOptions.
/**
* Returns a list of strings representing the rescale options for this transcode i.e. the ffmpeg -vf
* options used to show subtitles in either SSA/ASS or picture-based format and resize a video that's too wide and/or high for the specified renderer.
* If the renderer has no size limits, or there's no media metadata, or the video is within the renderer's
* size limits, an empty list is returned.
*
* @param dlna
* @param media metadata for the DLNA resource which is being transcoded
* @param params
* @return a {@link List} of <code>String</code>s representing the rescale options for this video,
* or an empty list if the video doesn't need to be resized.
* @throws java.io.IOException
*/
public List<String> getVideoFilterOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException {
List<String> videoFilterOptions = new ArrayList<>();
ArrayList<String> filterChain = new ArrayList<>();
ArrayList<String> scalePadFilterChain = new ArrayList<>();
final RendererConfiguration renderer = params.mediaRenderer;
boolean isMediaValid = media != null && media.isMediaparsed() && media.getHeight() != 0;
boolean isResolutionTooHighForRenderer = isMediaValid && !params.mediaRenderer.isResolutionCompatibleWithRenderer(media.getWidth(), media.getHeight());
int scaleWidth = 0;
int scaleHeight = 0;
if (media.getWidth() > 0 && media.getHeight() > 0) {
scaleWidth = media.getWidth();
scaleHeight = media.getHeight();
}
boolean is3D = media.is3d() && !media.stereoscopyIsAnaglyph();
// Make sure the aspect ratio is 16/9 if the renderer needs it.
boolean keepAR = (renderer.isKeepAspectRatio() || renderer.isKeepAspectRatioTranscoding()) && !media.is3dFullSbsOrOu() && !"16:9".equals(media.getAspectRatioContainer());
// Scale and pad the video if necessary
if (isResolutionTooHighForRenderer || (!renderer.isRescaleByRenderer() && renderer.isMaximumResolutionSpecified() && media.getWidth() < 720)) {
// Do not rescale for SD video and higher
if (media.is3dFullSbsOrOu()) {
scalePadFilterChain.add(String.format("scale=%1$d:%2$d", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
} else {
scalePadFilterChain.add(String.format("scale=iw*min(%1$d/iw\\,%2$d/ih):ih*min(%1$d/iw\\,%2$d/ih)", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
if (keepAR) {
scalePadFilterChain.add(String.format("pad=%1$d:%2$d:(%1$d-iw)/2:(%2$d-ih)/2", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
}
}
} else if (keepAR && isMediaValid) {
if ((media.getWidth() / (double) media.getHeight()) >= (16 / (double) 9)) {
scalePadFilterChain.add("pad=iw:iw/(16/9):0:(oh-ih)/2");
scaleHeight = (int) Math.round(scaleWidth / (16 / (double) 9));
} else {
scalePadFilterChain.add("pad=ih*(16/9):ih:(ow-iw)/2:0");
scaleWidth = (int) Math.round(scaleHeight * (16 / (double) 9));
}
scaleWidth = convertToModX(scaleWidth, 4);
scaleHeight = convertToModX(scaleHeight, 4);
// Make sure we didn't exceed the renderer's maximum resolution.
if (scaleHeight > renderer.getMaxVideoHeight() || scaleWidth > renderer.getMaxVideoWidth()) {
scaleHeight = renderer.getMaxVideoHeight();
scaleWidth = renderer.getMaxVideoWidth();
}
scalePadFilterChain.add("scale=" + scaleWidth + ":" + scaleHeight);
}
filterChain.addAll(scalePadFilterChain);
boolean override = true;
if (renderer instanceof RendererConfiguration.OutputOverride) {
RendererConfiguration.OutputOverride or = (RendererConfiguration.OutputOverride) renderer;
override = or.addSubtitles();
}
if (!isDisableSubtitles(params) && override) {
boolean isSubsManualTiming = true;
DLNAMediaSubtitle convertedSubs = dlna.getMediaSubtitle();
StringBuilder subsFilter = new StringBuilder();
if (params.sid != null && params.sid.getType().isText()) {
boolean isSubsASS = params.sid.getType() == SubtitleType.ASS;
String originalSubsFilename = null;
if (is3D) {
if (convertedSubs != null && convertedSubs.getConvertedFile() != null) {
// subs are already converted to 3D so use them
originalSubsFilename = convertedSubs.getConvertedFile().getAbsolutePath();
} else if (!isSubsASS) {
// When subs are not converted and they are not in the ASS format and video is 3D then subs need conversion to 3D
originalSubsFilename = SubtitleUtils.getSubtitles(dlna, media, params, configuration, SubtitleType.ASS).getAbsolutePath();
} else {
originalSubsFilename = params.sid.getExternalFile().getAbsolutePath();
}
} else if (params.sid.isExternal()) {
if (params.sid.isStreamable() && renderer.streamSubsForTranscodedVideo()) {
// when subs are streamable do not transcode them
originalSubsFilename = null;
} else {
originalSubsFilename = params.sid.getExternalFile().getAbsolutePath();
}
} else if (params.sid.isEmbedded()) {
originalSubsFilename = dlna.getFileName();
}
if (originalSubsFilename != null) {
subsFilter.append("subtitles=").append(StringUtil.ffmpegEscape(originalSubsFilename));
if (params.sid.isEmbedded()) {
subsFilter.append(":si=").append(params.sid.getId());
}
// Set the input subtitles character encoding if not UTF-8
if (!params.sid.isSubsUtf8()) {
if (isNotBlank(configuration.getSubtitlesCodepage())) {
subsFilter.append(":charenc=").append(configuration.getSubtitlesCodepage());
} else if (params.sid.getSubCharacterSet() != null) {
subsFilter.append(":charenc=").append(params.sid.getSubCharacterSet());
}
}
// If the FFmpeg font config is enabled than we need to add settings to the filter. TODO there could be also changed the font type. See http://ffmpeg.org/ffmpeg-filters.html#subtitles-1
if (configuration.isFFmpegFontConfig() && !is3D && !isSubsASS) {
// Do not force style for 3D videos and ASS subtitles
subsFilter.append(":force_style=");
subsFilter.append("'");
String fontName = configuration.getFont();
if (isNotBlank(fontName)) {
String font = CodecUtil.isFontRegisteredInOS(fontName);
if (font != null) {
subsFilter.append("Fontname=").append(font);
}
}
// XXX (valib) If the font size is not acceptable it could be calculated better taking in to account the original video size. Unfortunately I don't know how to do that.
subsFilter.append(",Fontsize=").append((int) 15 * Double.parseDouble(configuration.getAssScale()));
subsFilter.append(",PrimaryColour=").append(configuration.getSubsColor().getASSv4StylesHexValue());
subsFilter.append(",Outline=").append(configuration.getAssOutline());
subsFilter.append(",Shadow=").append(configuration.getAssShadow());
subsFilter.append(",MarginV=").append(configuration.getAssMargin());
subsFilter.append("'");
}
}
} else if (params.sid.getType().isPicture()) {
if (params.sid.getId() < 100) {
// Embedded
subsFilter.append("[0:v][0:s:").append(media.getSubtitleTracksList().indexOf(params.sid)).append("]overlay");
isSubsManualTiming = false;
} else {
// External
videoFilterOptions.add("-i");
videoFilterOptions.add(params.sid.getExternalFile().getAbsolutePath());
// this assumes the sub file is single-language
subsFilter.append("[0:v][1:s]overlay");
}
}
if (isNotBlank(subsFilter)) {
if (params.timeseek > 0 && isSubsManualTiming) {
// based on https://trac.ffmpeg.org/ticket/2067
filterChain.add("setpts=PTS+" + params.timeseek + "/TB");
}
filterChain.add(subsFilter.toString());
if (params.timeseek > 0 && isSubsManualTiming) {
// based on https://trac.ffmpeg.org/ticket/2067
filterChain.add("setpts=PTS-STARTPTS");
}
}
}
String overrideVF = renderer.getFFmpegVideoFilterOverride();
if (StringUtils.isNotEmpty(overrideVF)) {
filterChain.add(overrideVF);
}
// Convert 3D video to the other output 3D format or to 2D using "Output3DFormat = ml" or "Output3DFormat = mr" in the renderer conf
String stereoLayout = null;
String renderer3DOutputFormat = null;
if (media.get3DLayout() != null) {
stereoLayout = media.get3DLayout().toString().toLowerCase(Locale.ROOT);
renderer3DOutputFormat = params.mediaRenderer.getOutput3DFormat();
}
if (is3D && stereoLayout != null && isNotBlank(renderer3DOutputFormat) && !stereoLayout.equals(renderer3DOutputFormat)) {
filterChain.add("stereo3d=" + stereoLayout + ":" + renderer3DOutputFormat);
}
if (filterChain.size() > 0) {
videoFilterOptions.add("-filter_complex");
videoFilterOptions.add(StringUtils.join(filterChain, ","));
}
return videoFilterOptions;
}
Aggregations