use of net.pms.dlna.DLNAMediaSubtitle in project UniversalMediaServer by UniversalMediaServer.
the class FileUtil method browseFolderForSubtitles.
private static boolean browseFolderForSubtitles(File subFolder, File file, DLNAMediaInfo media, final boolean useCache) {
boolean found = false;
final Set<String> supported = SubtitleType.getSupportedFileExtensions();
File[] allSubs = null;
// TODO This caching scheme is very restrictive locking the whole cache
// while populating a single folder. A more effective solution should
// be implemented.
subtitleCacheLock.lock();
try {
if (useCache) {
allSubs = subtitleCache.get(subFolder);
}
if (allSubs == null) {
allSubs = subFolder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
String ext = FilenameUtils.getExtension(name).toLowerCase();
if ("sub".equals(ext)) {
// they'll come in unambiguously as vobsub via the idx file anyway
return replaceExtension(new File(dir, name), "idx", true, true) == null;
}
return supported.contains(ext);
}
});
if (allSubs != null) {
subtitleCache.put(subFolder, allSubs);
}
}
} finally {
subtitleCacheLock.unlock();
}
String fileName = getFileNameWithoutExtension(file.getName()).toLowerCase();
if (allSubs != null) {
for (File f : allSubs) {
if (f.isFile() && !f.isHidden()) {
String fName = f.getName().toLowerCase();
for (String ext : supported) {
if (fName.length() > ext.length() && fName.startsWith(fileName) && endsWithIgnoreCase(fName, "." + ext)) {
int a = fileName.length();
int b = fName.length() - ext.length() - 1;
String code = "";
if (a <= b) {
// handling case with several dots: <video>..<extension>
code = fName.substring(a, b);
}
if (code.startsWith(".")) {
code = code.substring(1);
}
boolean exists = false;
if (media != null) {
for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
if (f.equals(sub.getExternalFile())) {
exists = true;
} else if (equalsIgnoreCase(ext, "idx") && sub.getType() == SubtitleType.MICRODVD) {
// sub+idx => VOBSUB
sub.setType(SubtitleType.VOBSUB);
exists = true;
} else if (equalsIgnoreCase(ext, "sub") && sub.getType() == SubtitleType.VOBSUB) {
// VOBSUB
try {
sub.setExternalFile(f, null);
} catch (FileNotFoundException ex) {
LOGGER.warn("File not found during external subtitles scan: {}", ex.getMessage());
LOGGER.trace("", ex);
}
exists = true;
}
}
}
if (!exists) {
String forcedLang = null;
DLNAMediaSubtitle sub = new DLNAMediaSubtitle();
// fake id, not used
sub.setId(100 + (media == null ? 0 : media.getSubtitleTracksList().size()));
if (code.length() == 0 || !Iso639.codeIsValid(code)) {
sub.setLang(DLNAMediaSubtitle.UND);
sub.setType(SubtitleType.valueOfFileExtension(ext));
if (code.length() > 0) {
sub.setSubtitlesTrackTitleFromMetadata(code);
if (sub.getSubtitlesTrackTitleFromMetadata().contains("-")) {
String flavorLang = sub.getSubtitlesTrackTitleFromMetadata().substring(0, sub.getSubtitlesTrackTitleFromMetadata().indexOf('-'));
String flavorTitle = sub.getSubtitlesTrackTitleFromMetadata().substring(sub.getSubtitlesTrackTitleFromMetadata().indexOf('-') + 1);
if (Iso639.codeIsValid(flavorLang)) {
sub.setLang(flavorLang);
sub.setSubtitlesTrackTitleFromMetadata(flavorTitle);
forcedLang = flavorLang;
}
}
}
} else {
sub.setLang(code);
sub.setType(SubtitleType.valueOfFileExtension(ext));
forcedLang = code;
}
try {
sub.setExternalFile(f, forcedLang);
} catch (FileNotFoundException ex) {
LOGGER.warn("File not found during external subtitles scan: {}", ex.getMessage());
LOGGER.trace("", ex);
}
found = true;
if (media != null) {
media.getSubtitleTracksList().add(sub);
}
}
}
}
}
}
}
return found;
}
use of net.pms.dlna.DLNAMediaSubtitle in project UniversalMediaServer by UniversalMediaServer.
the class Request method answer.
/**
* Construct a proper HTTP response to a received request. After the response has been
* created, it is sent and the resulting {@link ChannelFuture} object is returned.
* See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">RFC-2616</a>
* for HTTP header field definitions.
* @param output The {@link HttpResponse} object that will be used to construct the response.
* @param startStopListenerDelegate The {@link StartStopListenerDelegate} object that is used
* to notify plugins that the {@link DLNAResource} is about to start playing.
* @throws IOException
*/
public void answer(OutputStream output, StartStopListenerDelegate startStopListenerDelegate) throws IOException {
this.output = output;
List<String> responseHeader = new ArrayList<>();
// 0 and above are valid Content-Length values, -1 means omit
long CLoverride = -2;
if (lowRange != 0 || highRange != 0) {
appendToHeader(responseHeader, http10 ? HTTP_206_OK_10 : HTTP_206_OK);
} else {
if (soapaction != null && soapaction.contains("X_GetFeatureList")) {
// If we don't return a 500 error, Samsung 2012 TVs time out.
appendToHeader(responseHeader, http10 ? HTTP_500_10 : HTTP_500);
} else {
appendToHeader(responseHeader, http10 ? HTTP_200_OK_10 : HTTP_200_OK);
}
}
StringBuilder response = new StringBuilder();
DLNAResource dlna = null;
boolean xbox360 = (mediaRenderer == null ? false : mediaRenderer.isXbox360());
// Samsung 2012 TVs have a problematic preceding slash that needs to be removed.
if (argument.startsWith("/")) {
LOGGER.trace("Stripping preceding slash from: " + argument);
argument = argument.substring(1);
}
if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) {
// Request to output a page to the HTML console.
appendToHeader(responseHeader, "Content-Type: text/html");
response.append(HTMLConsole.servePage(argument.substring(8)));
} else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) {
// Request to retrieve a file
/**
* Skip the leading "get/"
* e.g. "get/0$1$5$3$4/Foo.mp4" -> "0$1$5$3$4/Foo.mp4"
*
* ExSport: I spotted on Android it is asking for "/get/0$2$4$2$1$3" which generates exception with response:
* "Http: Response, HTTP/1.1, Status: Internal server error, URL: /get/0$2$4$2$1$3"
* This should fix it
*/
// Note: we intentionally include the trailing filename here because it may
// be used to reconstruct lost Temp items.
String id = argument.substring(argument.indexOf("get/") + 4);
// Some clients escape the separators in their request: unescape them.
id = id.replace("%24", "$");
// Retrieve the DLNAresource itself.
dlna = PMS.get().getRootFolder(mediaRenderer).getDLNAResource(id, mediaRenderer);
String fileName = id.substring(id.indexOf('/') + 1);
if (transferMode != null) {
appendToHeader(responseHeader, "TransferMode.DLNA.ORG: " + transferMode);
}
if (dlna != null && dlna.isFolder() && !fileName.startsWith("thumbnail0000")) {
// if we found a folder we MUST be asked for thumbnails
// otherwise this is not allowed
dlna = null;
}
if (dlna != null) {
// DLNAresource was found.
if (fileName.startsWith("thumbnail0000")) {
// This is a request for a thumbnail file.
DLNAImageProfile imageProfile = ImagesUtil.parseThumbRequest(fileName);
appendToHeader(responseHeader, "Content-Type: " + imageProfile.getMimeType());
appendToHeader(responseHeader, "Accept-Ranges: bytes");
appendToHeader(responseHeader, "Expires: " + getFUTUREDATE() + " GMT");
appendToHeader(responseHeader, "Connection: keep-alive");
DLNAThumbnailInputStream thumbInputStream;
if (!configuration.isShowCodeThumbs() && !dlna.isCodeValid(dlna)) {
thumbInputStream = dlna.getGenericThumbnailInputStream(null);
} else {
dlna.checkThumbnail();
thumbInputStream = dlna.fetchThumbnailInputStream();
}
if (dlna instanceof RealFile && FullyPlayed.isFullyPlayedThumbnail(((RealFile) dlna).getFile())) {
thumbInputStream = FullyPlayed.addFullyPlayedOverlay(thumbInputStream);
}
inputStream = thumbInputStream.transcode(imageProfile, mediaRenderer != null ? mediaRenderer.isThumbnailPadding() : false);
if (contentFeatures != null) {
appendToHeader(responseHeader, "ContentFeatures.DLNA.ORG: " + dlna.getDlnaContentFeatures(imageProfile, true));
}
if (inputStream != null && (lowRange > 0 || highRange > 0)) {
if (lowRange > 0) {
inputStream.skip(lowRange);
}
inputStream = DLNAResource.wrap(inputStream, highRange, lowRange);
}
appendToHeader(responseHeader, "Accept-Ranges: bytes");
appendToHeader(responseHeader, "Connection: keep-alive");
} else if (dlna.getMedia() != null && dlna.getMedia().getMediaType() == MediaType.IMAGE && dlna.isCodeValid(dlna)) {
// This is a request for an image
DLNAImageProfile imageProfile = ImagesUtil.parseImageRequest(fileName, null);
if (imageProfile == null) {
// Parsing failed for some reason, we'll have to pick a profile
if (dlna.getMedia().getImageInfo() != null && dlna.getMedia().getImageInfo().getFormat() != null) {
switch(dlna.getMedia().getImageInfo().getFormat()) {
case GIF:
imageProfile = DLNAImageProfile.GIF_LRG;
break;
case PNG:
imageProfile = DLNAImageProfile.PNG_LRG;
break;
default:
imageProfile = DLNAImageProfile.JPEG_LRG;
}
} else {
imageProfile = DLNAImageProfile.JPEG_LRG;
}
}
appendToHeader(responseHeader, "Content-Type: " + imageProfile.getMimeType());
appendToHeader(responseHeader, "Accept-Ranges: bytes");
appendToHeader(responseHeader, "Expires: " + getFUTUREDATE() + " GMT");
appendToHeader(responseHeader, "Connection: keep-alive");
try {
InputStream imageInputStream;
if (dlna.getPlayer() instanceof ImagePlayer) {
ProcessWrapper transcodeProcess = dlna.getPlayer().launchTranscode(dlna, dlna.getMedia(), new OutputParams(configuration));
imageInputStream = transcodeProcess != null ? transcodeProcess.getInputStream(0) : null;
} else {
imageInputStream = dlna.getInputStream();
}
if (imageInputStream == null) {
LOGGER.warn("Input stream returned for \"{}\" was null, no image will be sent to renderer", fileName);
} else {
inputStream = DLNAImageInputStream.toImageInputStream(imageInputStream, imageProfile, false);
if (contentFeatures != null) {
appendToHeader(responseHeader, "ContentFeatures.DLNA.ORG: " + dlna.getDlnaContentFeatures(imageProfile, false));
}
if (inputStream != null && (lowRange > 0 || highRange > 0)) {
if (lowRange > 0) {
inputStream.skip(lowRange);
}
inputStream = DLNAResource.wrap(inputStream, highRange, lowRange);
}
appendToHeader(responseHeader, "Accept-Ranges: bytes");
appendToHeader(responseHeader, "Connection: keep-alive");
}
} catch (IOException e) {
appendToHeader(responseHeader, "Content-Length: 0");
appendToHeader(responseHeader, "");
responseHeader.set(0, http10 ? HTTP_415_UNSUPPORTED_MEDIA_TYPE_10 : HTTP_415_UNSUPPORTED_MEDIA_TYPE);
sendHeader(responseHeader);
LOGGER.debug("Could not send image \"{}\": {}", dlna.getName(), e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
LOGGER.trace("", e);
return;
}
} else if (dlna.getMedia() != null && fileName.contains("subtitle0000") && dlna.isCodeValid(dlna)) {
// This is a request for a subtitles file
appendToHeader(responseHeader, "Content-Type: text/plain");
appendToHeader(responseHeader, "Expires: " + getFUTUREDATE() + " GMT");
DLNAMediaSubtitle sub = dlna.getMediaSubtitle();
if (sub != null) {
// http://www.ps3mediaserver.org/forum/viewtopic.php?f=3&t=15805&p=75534#p75534
if (sub.isExternal()) {
try {
if (sub.getType() == SubtitleType.SUBRIP && mediaRenderer.isRemoveTagsFromSRTsubs()) {
// remove tags from .srt subs when renderer doesn't support them
inputStream = SubtitleUtils.removeSubRipTags(sub.getExternalFile());
} else {
inputStream = new FileInputStream(sub.getExternalFile());
}
LOGGER.trace("Loading external subtitles file: {}", sub);
} catch (IOException ioe) {
LOGGER.debug("Couldn't load external subtitles file: {}\nCause: {}", sub, ioe.getMessage());
LOGGER.trace("", ioe);
}
} else {
LOGGER.trace("Not loading external subtitles file because it is embedded: {}", sub);
}
} else {
LOGGER.trace("Not loading external subtitles because dlna.getMediaSubtitle() returned null");
}
} else if (dlna.isCodeValid(dlna)) {
// This is a request for a regular file.
DLNAResource.Rendering origRendering = null;
if (!mediaRenderer.equals(dlna.getDefaultRenderer())) {
// Adjust rendering details for this renderer
origRendering = dlna.updateRendering(mediaRenderer);
}
String name = dlna.getDisplayName(mediaRenderer);
if (dlna.isNoName()) {
name = dlna.getName() + " " + dlna.getDisplayName(mediaRenderer);
}
inputStream = dlna.getInputStream(Range.create(lowRange, highRange, timeseek, timeRangeEnd), mediaRenderer);
if (dlna.isResume()) {
// Update timeseek to possibly adjusted resume time
timeseek = dlna.getResume().getTimeOffset() / (double) 1000;
}
if (inputStream == null) {
// No inputStream indicates that transcoding / remuxing probably crashed.
LOGGER.error("There is no inputstream to return for " + name);
} else {
startStopListenerDelegate.start(dlna);
appendToHeader(responseHeader, "Content-Type: " + getRendererMimeType(dlna.mimeType(), mediaRenderer, dlna.getMedia()));
if (dlna.getMedia() != null && !configuration.isDisableSubtitles() && dlna.getMediaSubtitle() != null && dlna.getMediaSubtitle().isStreamable()) {
// Some renderers (like Samsung devices) allow a custom header for a subtitle URL
String subtitleHttpHeader = mediaRenderer.getSubtitleHttpHeader();
if (isNotBlank(subtitleHttpHeader)) {
// Device allows a custom subtitle HTTP header; construct it
DLNAMediaSubtitle sub = dlna.getMediaSubtitle();
String subtitleUrl;
String subExtension = sub.getType().getExtension();
if (isNotBlank(subExtension)) {
subExtension = "." + subExtension;
}
subtitleUrl = "http://" + PMS.get().getServer().getHost() + ':' + PMS.get().getServer().getPort() + "/get/" + id.substring(0, id.indexOf('/')) + "/subtitle0000" + subExtension;
appendToHeader(responseHeader, subtitleHttpHeader + ": " + subtitleUrl);
} else {
LOGGER.trace("Did not send subtitle headers because mediaRenderer.getSubtitleHttpHeader() returned {}", subtitleHttpHeader == null ? "null" : "\"" + subtitleHttpHeader + "\"");
}
} else if (LOGGER.isTraceEnabled()) {
ArrayList<String> reasons = new ArrayList<>();
if (dlna.getMedia() == null) {
reasons.add("dlna.getMedia() is null");
}
if (configuration.isDisableSubtitles()) {
reasons.add("configuration.isDisabledSubtitles() is true");
}
if (dlna.getMediaSubtitle() == null) {
reasons.add("dlna.getMediaSubtitle() is null");
} else if (!dlna.getMediaSubtitle().isStreamable()) {
reasons.add("dlna.getMediaSubtitle().isStreamable() is false");
}
LOGGER.trace("Did not send subtitle headers because {}", StringUtil.createReadableCombinedString(reasons));
}
// Response generation:
// We use -1 for arithmetic convenience but don't send it as a value.
// If Content-Length < 0 we omit it, for Content-Range we use '*' to signify unspecified.
boolean chunked = mediaRenderer.isChunkedTransfer();
// Determine the total size. Note: when transcoding the length is
// not known in advance, so DLNAMediaInfo.TRANS_SIZE will be returned instead.
long totalsize = dlna.length(mediaRenderer);
if (chunked && totalsize == DLNAMediaInfo.TRANS_SIZE) {
// In chunked mode we try to avoid arbitrary values.
totalsize = -1;
}
long remaining = totalsize - lowRange;
long requested = highRange - lowRange;
if (requested != 0) {
// Determine the range (i.e. smaller of known or requested bytes)
long bytes = remaining > -1 ? remaining : inputStream.available();
if (requested > 0 && bytes > requested) {
bytes = requested + 1;
}
// Calculate the corresponding highRange (this is usually redundant).
highRange = lowRange + bytes - (bytes > 0 ? 1 : 0);
LOGGER.trace((chunked ? "Using chunked response. " : "") + "Sending " + bytes + " bytes.");
appendToHeader(responseHeader, "Content-Range: bytes " + lowRange + "-" + (highRange > -1 ? highRange : "*") + "/" + (totalsize > -1 ? totalsize : "*"));
// mode if the request is open-ended and totalsize is unknown we omit it.
if (chunked && requested < 0 && totalsize < 0) {
CLoverride = -1;
} else {
CLoverride = bytes;
}
} else {
// Content-Length refers to the total remaining size of the stream here.
CLoverride = remaining;
}
if (contentFeatures != null) {
appendToHeader(responseHeader, "ContentFeatures.DLNA.ORG: " + dlna.getDlnaContentFeatures(mediaRenderer));
}
if (dlna.getPlayer() == null || xbox360) {
appendToHeader(responseHeader, "Accept-Ranges: bytes");
}
appendToHeader(responseHeader, "Connection: keep-alive");
}
if (origRendering != null) {
// Restore original rendering details
dlna.updateRendering(origRendering);
}
}
}
} else if ((method.equals("GET") || method.equals("HEAD")) && (argument.toLowerCase().endsWith(".png") || argument.toLowerCase().endsWith(".jpg") || argument.toLowerCase().endsWith(".jpeg"))) {
if (argument.toLowerCase().endsWith(".png")) {
appendToHeader(responseHeader, "Content-Type: image/png");
} else {
appendToHeader(responseHeader, "Content-Type: image/jpeg");
}
appendToHeader(responseHeader, "Accept-Ranges: bytes");
appendToHeader(responseHeader, "Connection: keep-alive");
appendToHeader(responseHeader, "Expires: " + getFUTUREDATE() + " GMT");
inputStream = getResourceInputStream(argument);
} else if ((method.equals("GET") || method.equals("HEAD")) && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) {
appendToHeader(responseHeader, CONTENT_TYPE);
appendToHeader(responseHeader, "Cache-Control: no-cache");
appendToHeader(responseHeader, "Expires: 0");
appendToHeader(responseHeader, "Accept-Ranges: bytes");
appendToHeader(responseHeader, "Connection: keep-alive");
inputStream = getResourceInputStream((argument.equals("description/fetch") ? "PMS.xml" : argument));
if (argument.equals("description/fetch")) {
byte[] b = new byte[inputStream.available()];
inputStream.read(b);
String s = new String(b, StandardCharsets.UTF_8);
// .substring(0, PMS.get().usn().length()-2));
s = s.replace("[uuid]", PMS.get().usn());
if (PMS.get().getServer().getHost() != null) {
s = s.replace("[host]", PMS.get().getServer().getHost());
s = s.replace("[port]", "" + PMS.get().getServer().getPort());
}
if (xbox360) {
LOGGER.debug("DLNA changes for Xbox 360");
s = s.replace("Universal Media Server", configuration.getServerDisplayName() + " : Windows Media Connect");
s = s.replace("<modelName>UMS</modelName>", "<modelName>Windows Media Connect</modelName>");
s = s.replace("<serviceList>", "<serviceList>" + CRLF + "<service>" + CRLF + "<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>" + CRLF + "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>" + CRLF + "<SCPDURL>/upnp/mrr/scpd</SCPDURL>" + CRLF + "<controlURL>/upnp/mrr/control</controlURL>" + CRLF + "</service>" + CRLF);
} else {
s = s.replace("Universal Media Server", configuration.getServerDisplayName());
}
inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8));
}
} else if (method.equals("POST") && (argument.contains("MS_MediaReceiverRegistrar_control") || argument.contains("mrr/control"))) {
appendToHeader(responseHeader, CONTENT_TYPE_UTF8);
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
if (soapaction != null && soapaction.contains("IsAuthorized")) {
response.append(HTTPXMLHelper.XBOX_360_2);
response.append(CRLF);
} else if (soapaction != null && soapaction.contains("IsValidated")) {
response.append(HTTPXMLHelper.XBOX_360_1);
response.append(CRLF);
}
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
} else if (method.equals("POST") && argument.endsWith("upnp/control/connection_manager")) {
appendToHeader(responseHeader, CONTENT_TYPE_UTF8);
if (soapaction != null && soapaction.contains("ConnectionManager:1#GetProtocolInfo")) {
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.PROTOCOLINFO_RESPONSE);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
}
} else if (method.equals("POST") && argument.endsWith("upnp/control/content_directory")) {
appendToHeader(responseHeader, CONTENT_TYPE_UTF8);
if (soapaction != null && soapaction.contains("ContentDirectory:1#GetSystemUpdateID")) {
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_HEADER);
response.append(CRLF);
response.append("<Id>").append(DLNAResource.getSystemUpdateId()).append("</Id>");
response.append(CRLF);
response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
} else if (soapaction != null && soapaction.contains("ContentDirectory:1#GetSortCapabilities")) {
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SORTCAPS_RESPONSE);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
} else if (soapaction != null && soapaction.contains("ContentDirectory:1#X_GetFeatureList")) {
// Added for Samsung 2012 TVs
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SAMSUNG_ERROR_RESPONSE);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
} else if (soapaction != null && soapaction.contains("ContentDirectory:1#GetSearchCapabilities")) {
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SEARCHCAPS_RESPONSE);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
} else if (soapaction != null && (soapaction.contains("ContentDirectory:1#Browse") || soapaction.contains("ContentDirectory:1#Search"))) {
objectID = getEnclosingValue(content, "<ObjectID", "</ObjectID>");
String containerID = null;
if ((objectID == null || objectID.length() == 0)) {
containerID = getEnclosingValue(content, "<ContainerID", "</ContainerID>");
if (containerID == null || (xbox360 && !containerID.contains("$"))) {
objectID = "0";
} else {
objectID = containerID;
containerID = null;
}
}
Object sI = getEnclosingValue(content, "<StartingIndex", "</StartingIndex>");
Object rC = getEnclosingValue(content, "<RequestedCount", "</RequestedCount>");
browseFlag = getEnclosingValue(content, "<BrowseFlag", "</BrowseFlag>");
if (sI != null) {
startingIndex = Integer.parseInt(sI.toString());
}
if (rC != null) {
requestCount = Integer.parseInt(rC.toString());
}
response.append(HTTPXMLHelper.XML_HEADER);
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
response.append(CRLF);
if (soapaction.contains("ContentDirectory:1#Search")) {
response.append(HTTPXMLHelper.SEARCHRESPONSE_HEADER);
} else {
response.append(HTTPXMLHelper.BROWSERESPONSE_HEADER);
}
response.append(CRLF);
response.append(HTTPXMLHelper.RESULT_HEADER);
response.append(HTTPXMLHelper.DIDL_HEADER);
boolean browseDirectChildren = browseFlag != null && browseFlag.equals("BrowseDirectChildren");
if (soapaction.contains("ContentDirectory:1#Search")) {
browseDirectChildren = true;
}
// Xbox 360 virtual containers ... d'oh!
String searchCriteria = null;
if (xbox360 && configuration.getUseCache() && PMS.get().getLibrary() != null && containerID != null) {
if (containerID.equals("7") && PMS.get().getLibrary().getAlbumFolder() != null) {
objectID = PMS.get().getLibrary().getAlbumFolder().getResourceId();
} else if (containerID.equals("6") && PMS.get().getLibrary().getArtistFolder() != null) {
objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
} else if (containerID.equals("5") && PMS.get().getLibrary().getGenreFolder() != null) {
objectID = PMS.get().getLibrary().getGenreFolder().getResourceId();
} else if (containerID.equals("F") && PMS.get().getLibrary().getPlaylistFolder() != null) {
objectID = PMS.get().getLibrary().getPlaylistFolder().getResourceId();
} else if (containerID.equals("4") && PMS.get().getLibrary().getAllFolder() != null) {
objectID = PMS.get().getLibrary().getAllFolder().getResourceId();
} else if (containerID.equals("1")) {
String artist = getEnclosingValue(content, "upnp:artist = "", "")");
if (artist != null) {
objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
searchCriteria = artist;
}
}
} else if (soapaction.contains("ContentDirectory:1#Search")) {
searchCriteria = getEnclosingValue(content, "<SearchCriteria", "</SearchCriteria>");
}
List<DLNAResource> files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(objectID, browseDirectChildren, startingIndex, requestCount, mediaRenderer, searchCriteria);
if (searchCriteria != null && files != null) {
UMSUtils.postSearch(files, searchCriteria);
if (xbox360) {
if (files.size() > 0) {
files = files.get(0).getChildren();
}
}
}
int minus = 0;
if (files != null) {
for (DLNAResource uf : files) {
if (xbox360 && containerID != null) {
uf.setFakeParentId(containerID);
}
if (uf.isCompatible(mediaRenderer) && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer)) || // all possible combination not only those supported by renderer because the renderer setting could be wrong.
files.get(0).getParent() instanceof FileTranscodeVirtualFolder) {
response.append(uf.getDidlString(mediaRenderer));
} else {
minus++;
}
}
}
response.append(HTTPXMLHelper.DIDL_FOOTER);
response.append(HTTPXMLHelper.RESULT_FOOTER);
response.append(CRLF);
int filessize = 0;
if (files != null) {
filessize = files.size();
}
response.append("<NumberReturned>").append(filessize - minus).append("</NumberReturned>");
response.append(CRLF);
DLNAResource parentFolder = null;
if (files != null && filessize > 0) {
parentFolder = files.get(0).getParent();
} else {
parentFolder = PMS.get().getRootFolder(mediaRenderer).getDLNAResource(objectID, mediaRenderer);
}
if (browseDirectChildren && mediaRenderer.isUseMediaInfo() && mediaRenderer.isDLNATreeHack()) {
// with the new parser, files are parsed and analyzed *before*
// creating the DLNA tree, every 10 items (the ps3 asks 10 by 10),
// so we do not know exactly the total number of items in the DLNA folder to send
// (regular files, plus the #transcode folder, maybe the #imdb one, also files can be
// invalidated and hidden if format is broken or encrypted, etc.).
// let's send a fake total size to force the renderer to ask following items
// returns 11 when 10 asked
int totalCount = startingIndex + requestCount + 1;
// If no more elements, send the startingIndex
if (filessize - minus <= 0) {
totalCount = startingIndex;
}
response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>");
} else if (browseDirectChildren) {
response.append("<TotalMatches>").append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus).append("</TotalMatches>");
} else {
// From upnp spec: If BrowseMetadata is specified in the BrowseFlags then TotalMatches = 1
response.append("<TotalMatches>1</TotalMatches>");
}
response.append(CRLF);
response.append("<UpdateID>");
if (parentFolder != null) {
response.append(parentFolder.getUpdateId());
} else {
response.append('1');
}
response.append("</UpdateID>");
response.append(CRLF);
if (soapaction.contains("ContentDirectory:1#Search")) {
response.append(HTTPXMLHelper.SEARCHRESPONSE_FOOTER);
} else {
response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER);
}
response.append(CRLF);
response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
response.append(CRLF);
}
} else if (method.equals("SUBSCRIBE")) {
if (soapaction == null) {
// Ignore this
return;
}
appendToHeader(responseHeader, CONTENT_TYPE_UTF8);
appendToHeader(responseHeader, "Content-Length: 0");
appendToHeader(responseHeader, "Connection: close");
appendToHeader(responseHeader, "SID: " + PMS.get().usn());
appendToHeader(responseHeader, "Server: " + PMS.get().getServerName());
appendToHeader(responseHeader, "Timeout: Second-1800");
appendToHeader(responseHeader, "");
sendHeader(responseHeader);
responseHeader.clear();
if (soapaction != null) {
String cb = soapaction.replace("<", "").replace(">", "");
try {
URL soapActionUrl = new URL(cb);
String addr = soapActionUrl.getHost();
int port = soapActionUrl.getPort();
try (Socket sock = new Socket(addr, port)) {
OutputStream out = sock.getOutputStream();
sendLine(out, "NOTIFY /" + argument + " HTTP/1.1");
sendLine(out, "SID: " + PMS.get().usn());
sendLine(out, "SEQ: " + 0);
sendLine(out, "NT: upnp:event");
sendLine(out, "NTS: upnp:propchange");
sendLine(out, "HOST: " + addr + ":" + port);
sendLine(out, CONTENT_TYPE_UTF8);
}
} catch (MalformedURLException ex) {
LOGGER.debug("Cannot parse address and port from soap action \"" + soapaction + "\"", ex);
}
} else {
LOGGER.debug("Expected soap action in request");
}
if (argument.contains("connection_manager")) {
response.append(HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ConnectionManager:1"));
response.append(HTTPXMLHelper.eventProp("SinkProtocolInfo"));
response.append(HTTPXMLHelper.eventProp("SourceProtocolInfo"));
response.append(HTTPXMLHelper.eventProp("CurrentConnectionIDs"));
response.append(HTTPXMLHelper.EVENT_FOOTER);
} else if (argument.contains("content_directory")) {
response.append(HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ContentDirectory:1"));
response.append(HTTPXMLHelper.eventProp("TransferIDs"));
response.append(HTTPXMLHelper.eventProp("ContainerUpdateIDs"));
response.append(HTTPXMLHelper.eventProp("SystemUpdateID", "" + DLNAResource.getSystemUpdateId()));
response.append(HTTPXMLHelper.EVENT_FOOTER);
}
}
appendToHeader(responseHeader, "Server: " + PMS.get().getServerName());
if (response.length() > 0) {
// A response message was constructed; convert it to data ready to be sent.
byte[] responseData = response.toString().getBytes("UTF-8");
appendToHeader(responseHeader, "Content-Length: " + responseData.length);
appendToHeader(responseHeader, "");
sendHeader(responseHeader);
// HEAD requests only require headers to be set, no need to set contents.
if (!method.equals("HEAD")) {
output.write(responseData);
// LOGGER.trace(response.toString());
}
} else if (inputStream != null) {
if (CLoverride > -2) {
// Content-Length override has been set, send or omit as appropriate
if (CLoverride > -1 && CLoverride != DLNAMediaInfo.TRANS_SIZE) {
// Since PS3 firmware 2.50, it is wiser not to send an arbitrary Content-Length,
// as the PS3 will display a network error and request the last seconds of the
// transcoded video. Better to send no Content-Length at all.
appendToHeader(responseHeader, "Content-Length: " + CLoverride);
}
} else {
int cl = inputStream.available();
LOGGER.trace("Available Content-Length: " + cl);
appendToHeader(responseHeader, "Content-Length: " + cl);
}
if (timeseek > 0 && dlna != null) {
// Add timeseek information headers.
String timeseekValue = StringUtil.formatDLNADuration(timeseek);
String timetotalValue = dlna.getMedia().getDurationString();
appendToHeader(responseHeader, "TimeSeekRange.dlna.org: npt=" + timeseekValue + "-" + timetotalValue + "/" + timetotalValue);
appendToHeader(responseHeader, "X-Seek-Range: npt=" + timeseekValue + "-" + timetotalValue + "/" + timetotalValue);
}
// Send the response headers to the client.
appendToHeader(responseHeader, "");
sendHeader(responseHeader);
long sendB = 0;
if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) {
// , ((lowRange > 0 && highRange > 0)?(highRange-lowRange):-1)
sendB = sendBytes(inputStream);
}
if (sendB > -1) {
LOGGER.trace("Sending stream: {} bytes of {}", sendB, argument);
} else {
// Premature end
startStopListenerDelegate.stop();
}
} else {
// inputStream is null
appendToHeader(responseHeader, "Content-Length: 0");
appendToHeader(responseHeader, "");
responseHeader.set(0, http10 ? HTTP_204_NO_CONTENT_10 : HTTP_204_NO_CONTENT);
sendHeader(responseHeader);
}
if (LOGGER.isTraceEnabled()) {
// Log trace information
StringBuilder header = new StringBuilder();
for (int i = 0; i < responseHeader.size(); i++) {
if (isNotBlank(responseHeader.get(i))) {
header.append(" ").append(responseHeader.get(i)).append("\n");
}
}
String rendererName;
if (mediaRenderer != null) {
if (isNotBlank(mediaRenderer.getRendererName())) {
if (isBlank(mediaRenderer.getConfName()) || mediaRenderer.getRendererName().equals(mediaRenderer.getConfName())) {
rendererName = mediaRenderer.getRendererName();
} else {
rendererName = mediaRenderer.getRendererName() + " [" + mediaRenderer.getConfName() + "]";
}
} else if (isNotBlank(mediaRenderer.getConfName())) {
rendererName = mediaRenderer.getConfName();
} else {
rendererName = "Unnamed";
}
} else {
rendererName = "Unknown";
}
if (method.equals("HEAD")) {
LOGGER.trace("HEAD only response sent to {}:\n\nHEADER:\n{}", rendererName, header);
} else {
String formattedResponse = null;
if (isNotBlank(response)) {
try {
formattedResponse = StringUtil.prettifyXML(response.toString(), 4);
} catch (SAXException | ParserConfigurationException | XPathExpressionException | TransformerException e) {
formattedResponse = " Content isn't valid XML, using text formatting: " + e.getMessage() + "\n";
formattedResponse += " " + response.toString().replaceAll("\n", "\n ");
}
}
if (isNotBlank(formattedResponse)) {
LOGGER.trace("Response sent to {}:\n\nHEADER:\n{}\nCONTENT:\n{}", rendererName, header, formattedResponse);
Matcher matcher = DIDL_PATTERN.matcher(response);
if (matcher.find()) {
try {
LOGGER.trace("The unescaped <Result> sent to {} is:\n{}", mediaRenderer.getConfName(), StringUtil.prettifyXML(StringEscapeUtils.unescapeXml(matcher.group(1)), 2));
} catch (SAXException | ParserConfigurationException | XPathExpressionException | TransformerException e) {
LOGGER.warn("Failed to prettify DIDL-Lite document: {}", e.getMessage());
LOGGER.trace("", e);
}
}
} else if (inputStream != null && !responseHeader.contains("Content-Length: 0")) {
LOGGER.trace("Transfer response sent to {}:\n\nHEADER:\n{}", rendererName, header);
} else {
LOGGER.trace("Empty response sent to {}:\n\nHEADER:\n{}", rendererName, header);
}
}
}
}
use of net.pms.dlna.DLNAMediaSubtitle in project UniversalMediaServer by UniversalMediaServer.
the class Player method setSubtitleOutputParameters.
/**
* This method populates the supplied {@link OutputParams} object with the correct subtitles (sid)
* based on the given filename, its MediaInfo metadata and PMS configuration settings.
*
* TODO: Rewrite this crazy method to be more concise and logical.
*
* @param fileName
* The file name used to determine the availability of subtitles.
* @param media
* The MediaInfo metadata for the file.
* @param params
* The parameters to populate.
*/
public static void setSubtitleOutputParameters(String fileName, DLNAMediaInfo media, OutputParams params) {
// Use device-specific pms conf
PmsConfiguration configuration = PMS.getConfiguration(params);
String currentLang = null;
DLNAMediaSubtitle matchedSub = null;
if (params.aid != null) {
currentLang = params.aid.getLang();
}
if (params.sid != null && params.sid.getId() == -1) {
LOGGER.trace("Don't want subtitles!");
params.sid = null;
return;
}
/**
* Check for live subtitles
*/
if (params.sid != null && !StringUtils.isEmpty(params.sid.getLiveSubURL())) {
LOGGER.debug("Live subtitles " + params.sid.getLiveSubURL());
try {
matchedSub = params.sid;
String file = OpenSubtitle.fetchSubs(matchedSub.getLiveSubURL(), matchedSub.getLiveSubFile());
if (!StringUtils.isEmpty(file)) {
matchedSub.setExternalFile(new File(file), null);
params.sid = matchedSub;
return;
}
} catch (IOException e) {
}
}
StringTokenizer st = new StringTokenizer(configuration.getAudioSubLanguages(), ";");
/**
* Check for external and internal subtitles matching the user's language
* preferences
*/
boolean matchedInternalSubtitles = false;
boolean matchedExternalSubtitles = false;
while (st.hasMoreTokens()) {
String pair = st.nextToken();
if (pair.contains(",")) {
String audio = pair.substring(0, pair.indexOf(','));
String sub = pair.substring(pair.indexOf(',') + 1);
audio = audio.trim();
sub = sub.trim();
LOGGER.trace("Searching for a match for: " + currentLang + " with " + audio + " and " + sub);
if (Iso639.isCodesMatching(audio, currentLang) || (currentLang != null && audio.equals("*"))) {
if (sub.equals("off")) {
/**
* Ignore the "off" language for external subtitles if the user setting is enabled
* TODO: Prioritize multiple external subtitles properly instead of just taking the first one we load
*/
if (configuration.isForceExternalSubtitles()) {
for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
if (present_sub.getExternalFile() != null) {
matchedSub = present_sub;
matchedExternalSubtitles = true;
LOGGER.trace("Ignoring the \"off\" language because there are external subtitles");
break;
}
}
}
if (!matchedExternalSubtitles) {
matchedSub = new DLNAMediaSubtitle();
matchedSub.setLang("off");
}
} else {
for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
if (present_sub.matchCode(sub) || sub.equals("*")) {
if (present_sub.getExternalFile() != null) {
if (configuration.isAutoloadExternalSubtitles()) {
// Subtitle is external and we want external subtitles, look no further
matchedSub = present_sub;
LOGGER.trace("Matched external subtitles track: " + matchedSub);
break;
} else {
// Subtitle is external but we do not want external subtitles, keep searching
LOGGER.trace("External subtitles ignored because of user setting: " + present_sub);
}
} else if (!matchedInternalSubtitles) {
matchedSub = present_sub;
LOGGER.trace("Matched internal subtitles track: " + matchedSub);
if (configuration.isAutoloadExternalSubtitles()) {
// Subtitle is internal and we will wait to see if an external one is available instead
matchedInternalSubtitles = true;
} else {
// Subtitle is internal and we will use it
break;
}
}
}
}
}
if (matchedSub != null && !matchedInternalSubtitles) {
break;
}
}
}
}
/**
* Check for external subtitles that were skipped in the above code block
* because they didn't match language preferences, if there wasn't already
* a match and the user settings specify it.
*/
if (matchedSub == null && configuration.isForceExternalSubtitles()) {
for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
if (present_sub.getExternalFile() != null) {
matchedSub = present_sub;
LOGGER.trace("Matched external subtitles track that did not match language preferences: " + matchedSub);
break;
}
}
}
/**
* Disable chosen subtitles if the user has disabled all subtitles or
* if the language preferences have specified the "off" language.
*
* TODO: Can't we save a bunch of looping by checking for isDisableSubtitles
* just after the Live Subtitles check above?
*/
if (matchedSub != null && params.sid == null) {
if (configuration.isDisableSubtitles() || (matchedSub.getLang() != null && matchedSub.getLang().equals("off"))) {
LOGGER.trace("Disabled the subtitles: " + matchedSub);
} else {
params.sid = matchedSub;
}
}
/**
* Check for forced subtitles.
*/
if (!configuration.isDisableSubtitles() && params.sid == null && media != null) {
// Check for subtitles again
File video = new File(fileName);
FileUtil.isSubtitlesExists(video, media, false);
if (configuration.isAutoloadExternalSubtitles()) {
boolean forcedSubsFound = false;
// Priority to external subtitles
for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) {
st = new StringTokenizer(configuration.getForcedSubtitleTags(), ",");
while (sub.getSubtitlesTrackTitleFromMetadata() != null && st.hasMoreTokens()) {
String forcedTags = st.nextToken();
forcedTags = forcedTags.trim();
if (sub.getSubtitlesTrackTitleFromMetadata().toLowerCase().contains(forcedTags) && Iso639.isCodesMatching(sub.getLang(), configuration.getForcedSubtitleLanguage())) {
LOGGER.trace("Forcing preferred subtitles: " + sub.getLang() + "/" + sub.getSubtitlesTrackTitleFromMetadata());
LOGGER.trace("Forced subtitles track: " + sub);
if (sub.getExternalFile() != null) {
LOGGER.trace("Found external forced file: " + sub.getExternalFile().getAbsolutePath());
}
params.sid = sub;
forcedSubsFound = true;
break;
}
}
if (forcedSubsFound == true) {
break;
}
} else {
LOGGER.trace("Found subtitles track: " + sub);
if (sub.getExternalFile() != null) {
LOGGER.trace("Found external file: " + sub.getExternalFile().getAbsolutePath());
params.sid = sub;
break;
}
}
}
}
if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) {
return;
}
if (params.sid == null) {
st = new StringTokenizer(UMSUtils.getLangList(params.mediaRenderer), ",");
while (st.hasMoreTokens()) {
String lang = st.nextToken();
lang = lang.trim();
LOGGER.trace("Looking for a subtitle track with lang: " + lang);
for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
if (sub.matchCode(lang) && !(!configuration.isAutoloadExternalSubtitles() && sub.getExternalFile() != null)) {
params.sid = sub;
LOGGER.trace("Matched subtitles track: " + params.sid);
return;
}
}
}
}
}
}
use of net.pms.dlna.DLNAMediaSubtitle in project UniversalMediaServer by UniversalMediaServer.
the class AviSynthMEncoder method isCompatible.
/**
* {@inheritDoc}
*/
@Override
public boolean isCompatible(DLNAResource resource) {
Format format = resource.getFormat();
if (format != null) {
if (format.getIdentifier() == Format.Identifier.WEB) {
return false;
}
}
DLNAMediaSubtitle subtitle = resource.getMediaSubtitle();
// Uninitialized DLNAMediaSubtitle objects have a null language.
if (subtitle != null && subtitle.getLang() != null) {
// This engine only supports external subtitles
if (subtitle.getExternalFile() != null) {
return true;
}
return false;
}
try {
String audioTrackName = resource.getMediaAudio().toString();
String defaultAudioTrackName = resource.getMedia().getAudioTracksList().get(0).toString();
if (!audioTrackName.equals(defaultAudioTrackName)) {
// This engine only supports playback of the default audio track
return false;
}
} catch (NullPointerException e) {
LOGGER.trace("AviSynth/MEncoder cannot determine compatibility based on audio track for " + resource.getSystemName());
} catch (IndexOutOfBoundsException e) {
LOGGER.trace("AviSynth/MEncoder cannot determine compatibility based on default audio track for " + resource.getSystemName());
}
if (PlayerUtil.isVideo(resource, Format.Identifier.MKV) || PlayerUtil.isVideo(resource, Format.Identifier.MPG) || PlayerUtil.isVideo(resource, Format.Identifier.OGG)) {
return true;
}
return false;
}
use of net.pms.dlna.DLNAMediaSubtitle 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