/* * 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.network; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.TimeZone; import net.pms.PMS; import net.pms.configuration.RendererConfiguration; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAMediaSubtitle; import net.pms.dlna.DLNAResource; import net.pms.dlna.Range; import net.pms.external.StartStopListenerDelegate; import org.apache.commons.lang.StringUtils; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.stream.ChunkedStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class handles all forms of incoming HTTP requests by constructing a proper HTTP response. */ public class RequestV2 extends HTTPResource { private static final Logger logger = LoggerFactory.getLogger(RequestV2.class); private final static String CRLF = "\r\n"; private static SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); private static int BUFFER_SIZE = 8 * 1024; private static final int[] MULTIPLIER = new int[] { 1, 60, 3600, 24*3600}; private final String method; /** * A {@link String} that contains the argument with which this {@link RequestV2} was * created. It contains a command, a unique resource id and a resource name, all * separated by slashes. For example: "get/0$0$2$17/big_buck_bunny_1080p_h264.mov" or * "get/0$0$2$13/thumbnail0000Sintel.2010.1080p.mkv" */ private final String argument; private String soapaction; private String content; private String objectID; private int startingIndex; private int requestCount; private String browseFlag; /** * When sending an input stream, the lowRange indicates which byte to start from. */ private long lowRange; private InputStream inputStream; private RendererConfiguration mediaRenderer; private String transferMode; private String contentFeatures; private final Range.Time range = new Range.Time(); /** * When sending an input stream, the highRange indicates which byte to stop at. */ private long highRange; private boolean http10; public RendererConfiguration getMediaRenderer() { return mediaRenderer; } public void setMediaRenderer(RendererConfiguration mediaRenderer) { this.mediaRenderer = mediaRenderer; } public InputStream getInputStream() { return inputStream; } /** * When sending an input stream, the lowRange indicates which byte to start from. * @return The byte to start from */ public long getLowRange() { return lowRange; } /** * Set the byte from which to start when sending an input stream. This value will * be used to send a CONTENT_RANGE header with the response. * @param lowRange The byte to start from. */ public void setLowRange(long lowRange) { this.lowRange = lowRange; } public String getTransferMode() { return transferMode; } public void setTransferMode(String transferMode) { this.transferMode = transferMode; } public String getContentFeatures() { return contentFeatures; } public void setContentFeatures(String contentFeatures) { this.contentFeatures = contentFeatures; } public void setTimeRangeStart(Double timeseek) { this.range.setStart(timeseek); } public void setTimeRangeStartString(String str) { setTimeRangeStart(convertTime(str)); } public void setTimeRangeEnd(Double rangeEnd) { this.range.setEnd(rangeEnd); } public void setTimeRangeEndString(String str) { setTimeRangeEnd(convertTime(str)); } /** * When sending an input stream, the highRange indicates which byte to stop at. * @return The byte to stop at. */ public long getHighRange() { return highRange; } /** * Set the byte at which to stop when sending an input stream. This value will * be used to send a CONTENT_RANGE header with the response. * @param highRange The byte to stop at. */ public void setHighRange(long highRange) { this.highRange = highRange; } public boolean isHttp10() { return http10; } public void setHttp10(boolean http10) { this.http10 = http10; } /** * This class will construct and transmit a proper HTTP response to a given HTTP request. * Rewritten version of the {@link Request} class. * @param method The {@link String} that defines the HTTP method to be used. * @param argument The {@link String} containing instructions for PMS. It contains a command, * a unique resource id and a resource name, all separated by slashes. */ public RequestV2(String method, String argument) { this.method = method; this.argument = argument; } public String getSoapaction() { return soapaction; } public void setSoapaction(String soapaction) { this.soapaction = soapaction; } public String getTextContent() { return content; } public void setTextContent(String content) { this.content = content; } /** * Retrieves the HTTP method with which this {@link RequestV2} was created. * @return The (@link String} containing the HTTP method. */ public String getMethod() { return method; } /** * Retrieves the argument with which this {@link RequestV2} was created. It contains * a command, a unique resource id and a resource name, all separated by slashes. For * example: "get/0$0$2$17/big_buck_bunny_1080p_h264.mov" or "get/0$0$2$13/thumbnail0000Sintel.2010.1080p.mkv" * @return The {@link String} containing the argument. */ public String getArgument() { return argument; } /** * 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 RFC-2616 * for HTTP header field definitions. * @param output The {@link HttpResponse} object that will be used to construct the response. * @param e The {@link MessageEvent} object used to communicate with the client that sent * the request. * @param close Set to true to close the channel after sending the response. By default the * channel is not closed after sending. * @param startStopListenerDelegate The {@link StartStopListenerDelegate} object that is used * to notify plugins that the {@link DLNAResource} is about to start playing. * @return The {@link ChannelFuture} object via which the response was sent. * @throws IOException */ public ChannelFuture answer( HttpResponse output, MessageEvent e, final boolean close, final StartStopListenerDelegate startStopListenerDelegate) throws IOException { ChannelFuture future = null; long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit StringBuilder response = new StringBuilder(); DLNAResource dlna = null; boolean xbox = mediaRenderer.isXBOX(); if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) { // Request to output a page to the HTLM console. output.setHeader(HttpHeaders.Names.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 // Extract the resource id from the argument string. String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/")); // Some clients escape the separators in their request, unescape them. id = id.replace("%24", "$"); // Retrieve the DLNAresource itself. List files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer); if (transferMode != null) { output.setHeader("TransferMode.DLNA.ORG", transferMode); } if (files.size() == 1) { // DNLAresource was found. dlna = files.get(0); String fileName = argument.substring(argument.lastIndexOf("/") + 1); if (fileName.startsWith("thumbnail0000")) { // This is a request for a thumbnail file. output.setHeader(HttpHeaders.Names.CONTENT_TYPE, files.get(0).getThumbnailContentType()); output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); if (mediaRenderer.isMediaParserV2()) { dlna.checkThumbnail(); } inputStream = dlna.getThumbnailInputStream(); } else if (fileName.indexOf("subtitle0000") > -1) { // This is a request for a subtitle file output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain"); output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT"); List subs = dlna.getMedia().getSubtitlesCodes(); if (subs != null && !subs.isEmpty()) { // TODO: maybe loop subs to get the requested subtitle type instead of using the first one DLNAMediaSubtitle sub = subs.get(0); inputStream = new java.io.FileInputStream(sub.getFile()); } } else { // This is a request for a regular file. // If range has not been initialized yet and the DLNAResource has its // own start and end defined, initialize range with those values before // requesting the input stream. Range.Time splitRange = dlna.getSplitRange(); if (range.getStart() == null && splitRange.getStart() != null) { range.setStart(splitRange.getStart()); } if (range.getEnd() == null && splitRange.getEnd() != null) { range.setEnd(splitRange.getEnd()); } inputStream = dlna.getInputStream(Range.create(lowRange, highRange, range.getStart(), range.getEnd()), mediaRenderer); // Some renderers (like Samsung devices) allow a custom header for a subtitle URL String subtitleHttpHeader = mediaRenderer.getSubtitleHttpHeader(); if (subtitleHttpHeader != null && !"".equals(subtitleHttpHeader)) { // Device allows a custom subtitle HTTP header; construct it List subs = dlna.getMedia().getSubtitlesCodes(); if (subs != null && !subs.isEmpty()) { DLNAMediaSubtitle sub = subs.get(0); int type = sub.getType(); if (type < DLNAMediaSubtitle.subExtensions.length) { String strType = DLNAMediaSubtitle.subExtensions[type - 1]; String subtitleUrl = "http://" + PMS.get().getServer().getHost() + ':' + PMS.get().getServer().getPort() + "/get/" + id + "/subtitle0000." + strType; output.setHeader(subtitleHttpHeader, subtitleUrl); } } } String name = dlna.getDisplayName(mediaRenderer); if (inputStream == null) { // No inputStream indicates that transcoding / remuxing probably crashed. logger.error("There is no inputstream to return for " + name); } else { // Notify plugins that the DLNAresource is about to start playing startStopListenerDelegate.start(dlna); // Try to determine the content type of the file String rendererMimeType = getRendererMimeType(files.get(0).mimeType(), mediaRenderer); if (rendererMimeType != null && !"".equals(rendererMimeType)) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, rendererMimeType); } final DLNAMediaInfo media = dlna.getMedia(); if (media != null) { if (StringUtils.isNotBlank(media.getContainer())) { name += " [container: " + media.getContainer() + "]"; } if (StringUtils.isNotBlank(media.getCodecV())) { name += " [video: " + media.getCodecV() + "]"; } } PMS.get().getFrame().setStatusLine("Serving " + name); // 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."); output.setHeader(HttpHeaders.Names.CONTENT_RANGE, "bytes " + lowRange + "-" + (highRange > -1 ? highRange : "*") + "/" + (totalsize > -1 ? totalsize : "*")); // Content-Length refers to the current chunk size here, though in chunked // 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; } // Calculate the corresponding highRange (this is usually redundant). highRange = lowRange + CLoverride - (CLoverride > 0 ? 1 : 0); if (contentFeatures != null) { output.setHeader("ContentFeatures.DLNA.ORG", files.get(0).getDlnaContentFeatures()); } output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); } } } } 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")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/png"); } else { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/jpeg"); } output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT"); inputStream = getResourceInputStream(argument); } else if ((method.equals("GET") || method.equals("HEAD")) && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); output.setHeader(HttpHeaders.Names.CACHE_CONTROL, "no-cache"); output.setHeader(HttpHeaders.Names.EXPIRES, "0"); output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.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); s = s.replace("uuid:1234567890TOTO", PMS.get().usn());//.substring(0, PMS.get().usn().length()-2)); String profileName = PMS.getConfiguration().getProfileName(); if (PMS.get().getServer().getHost() != null) { s = s.replace("", PMS.get().getServer().getHost()); s = s.replace("", "" + PMS.get().getServer().getPort()); } if (xbox) { logger.debug("DLNA changes for Xbox360"); s = s.replace("PS3 Media Server", "PS3 Media Server [" + profileName + "] : Windows Media Connect"); s = s.replace("PMS", "Windows Media Connect"); s = s.replace("", "" + CRLF + "" + CRLF + "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + CRLF + "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar" + CRLF + "/upnp/mrr/scpd" + CRLF + "/upnp/mrr/control" + CRLF + "" + CRLF); } else { s = s.replace("PS3 Media Server", "PS3 Media Server [" + profileName + "]"); } if (!mediaRenderer.isPS3()) { // hacky stuff. replace the png icon by a jpeg one. Like mpeg2 remux, // really need a proper format compatibility list by renderer s = s.replace("image/png", "image/jpeg"); s = s.replace("/images/thumbnail-256.png", "/images/thumbnail-120.jpg"); s = s.replace(">256<", ">120<"); } response.append(s); inputStream = null; } } else if (method.equals("POST") && (argument.contains("MS_MediaReceiverRegistrar_control") || argument.contains("mrr/control"))) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); 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_2); response.append(CRLF); } else if (soapaction != null && soapaction.contains("IsValidated")) { response.append(HTTPXMLHelper.XBOX_1); response.append(CRLF); } response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (method.equals("POST") && argument.equals("upnp/control/connection_manager")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); if (soapaction.indexOf("ConnectionManager:1#GetProtocolInfo") > -1) { 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.equals("upnp/control/content_directory")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); if (soapaction.indexOf("ContentDirectory:1#GetSystemUpdateID") > -1) { 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("" + DLNAResource.getSystemUpdateId() + ""); response.append(CRLF); response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction.indexOf("ContentDirectory:1#GetSortCapabilities") > -1) { 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.indexOf("ContentDirectory:1#GetSearchCapabilities") > -1) { 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.contains("ContentDirectory:1#Browse") || soapaction.contains("ContentDirectory:1#Search")) { objectID = getEnclosingValue(content, "", ""); String containerID = null; if ((objectID == null || objectID.length() == 0) && xbox) { containerID = getEnclosingValue(content, "", ""); if (!containerID.contains("$")) { objectID = "0"; } else { objectID = containerID; containerID = null; } } Object sI = getEnclosingValue(content, "", ""); Object rC = getEnclosingValue(content, "", ""); browseFlag = getEnclosingValue(content, "", ""); 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); if (soapaction.contains("ContentDirectory:1#Search")) { browseFlag = "BrowseDirectChildren"; } // XBOX virtual containers ... d'oh! String searchCriteria = null; if (xbox && PMS.getConfiguration().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; } } } List files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(objectID, browseFlag != null && browseFlag.equals("BrowseDirectChildren"), startingIndex, requestCount, mediaRenderer); if (searchCriteria != null && files != null) { for (int i = files.size() - 1; i >= 0; i--) { if (!files.get(i).getName().equals(searchCriteria)) { files.remove(i); } } if (files.size() > 0) { files = files.get(0).getChildren(); } } int minus = 0; if (files != null) { for (DLNAResource uf : files) { if (xbox && containerID != null) { uf.setFakeParentId(containerID); } if (uf.isCompatible(mediaRenderer) && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer))) { response.append(uf.toString(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("").append(filessize - minus).append(""); response.append(CRLF); DLNAResource parentFolder = null; if (files != null && filessize > 0) { parentFolder = files.get(0).getParent(); } if (browseFlag != null && browseFlag.equals("BrowseDirectChildren") && mediaRenderer.isMediaParserV2() && 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 int totalCount = startingIndex + requestCount + 1; // returns 11 when 10 asked if (filessize - minus <= 0) // if no more elements, send the startingIndex { totalCount = startingIndex; } response.append("").append(totalCount).append(""); } else if (browseFlag != null && browseFlag.equals("BrowseDirectChildren")) { response.append("").append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus).append(""); } else { //from upnp spec: If BrowseMetadata is specified in the BrowseFlags then TotalMatches = 1 response.append("1"); } response.append(CRLF); response.append(""); if (parentFolder != null) { response.append(parentFolder.getUpdateId()); } else { response.append("1"); } response.append(""); 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); // logger.trace(response.toString()); } } else if (method.equals("SUBSCRIBE")) { output.setHeader("SID", PMS.get().usn()); output.setHeader("TIMEOUT", "Second-1800"); } else if (method.equals("NOTIFY")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml"); output.setHeader("NT", "upnp:event"); output.setHeader("NTS", "upnp:propchange"); output.setHeader("SID", PMS.get().usn()); output.setHeader("SEQ", "0"); response.append(""); response.append(""); response.append(""); response.append(""); response.append(""); response.append(""); response.append(""); response.append(""); response.append("").append(DLNAResource.getSystemUpdateId()).append(""); response.append(""); response.append(""); } output.setHeader("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"); output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + responseData.length); // HEAD requests only require headers to be set, no need to set contents. if (!method.equals("HEAD")) { // Not a HEAD request, so set the contents of the response. ChannelBuffer buf = ChannelBuffers.copiedBuffer(responseData); output.setContent(buf); } // Send the response to the client. future = e.getChannel().write(output); if (close) { // Close the channel after the response is sent. future.addListener(ChannelFutureListener.CLOSE); } } else if (inputStream != null) { // There is an input stream to send as a response. 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. output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + CLoverride); } } else { int cl = inputStream.available(); logger.trace("Available Content-Length: " + cl); output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + cl); } if (range.isStartOffsetAvailable() && dlna != null) { // Add timeseek information headers. String timeseekValue = DLNAMediaInfo.getDurationString(range.getStartOrZero()); String timetotalValue = dlna.getMedia().getDurationString(); String timeEndValue = range.isEndLimitAvailable() ? DLNAMediaInfo.getDurationString(range.getEnd()) : timetotalValue; output.setHeader("TimeSeekRange.dlna.org", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue); output.setHeader("X-Seek-Range", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue); } // Send the response headers to the client. future = e.getChannel().write(output); if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) { // Send the response body to the client in chunks. ChannelFuture chunkWriteFuture = e.getChannel().write(new ChunkedStream(inputStream, BUFFER_SIZE)); // Add a listener to clean up after sending the entire response body. chunkWriteFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { try { PMS.get().getRegistry().reenableGoToSleep(); inputStream.close(); } catch (IOException e) { } // Always close the channel after the response is sent because of // a freeze at the end of video when the channel is not closed. future.getChannel().close(); startStopListenerDelegate.stop(); } }); } else { // HEAD method is being used, so simply clean up after the response was sent. try { PMS.get().getRegistry().reenableGoToSleep(); inputStream.close(); } catch (IOException ioe) { } if (close) { // Close the channel after the response is sent future.addListener(ChannelFutureListener.CLOSE); } startStopListenerDelegate.stop(); } } else { // No response data and no input stream. Seems we are merely serving up headers. if (lowRange > 0 && highRange > 0) { // FIXME: There is no content, so why set a length? output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + (highRange - lowRange + 1)); } else { output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); } // Send the response headers to the client. future = e.getChannel().write(output); if (close) { // Close the channel after the response is sent. future.addListener(ChannelFutureListener.CLOSE); } } // Log trace information Iterator it = output.getHeaderNames().iterator(); while (it.hasNext()) { String headerName = it.next(); logger.trace("Sent to socket: " + headerName + ": " + output.getHeader(headerName)); } return future; } /** * Returns a date somewhere in the far future. * @return The {@link String} containing the date */ private String getFUTUREDATE() { sdf.setTimeZone(TimeZone.getTimeZone("GMT")); return sdf.format(new Date(10000000000L + System.currentTimeMillis())); } /** * Returns the string value that is enclosed by the left and right tag in a content string. * Only the first match of each tag is used to determine positions. If either of the tags * cannot be found, null is returned. * @param content The entire {@link String} that needs to be searched for the left and right tag. * @param leftTag The {@link String} determining the match for the left tag. * @param rightTag The {@link String} determining the match for the right tag. * @return The {@link String} that was enclosed by the left and right tag. */ private String getEnclosingValue(String content, String leftTag, String rightTag) { String result = null; int leftTagPos = content.indexOf(leftTag); int rightTagPos = content.indexOf(rightTag, leftTagPos + 1); if (leftTagPos > -1 && rightTagPos > leftTagPos) { result = content.substring(leftTagPos + leftTag.length(), rightTagPos); } return result; } /** * Parse as double, or if it's not just one number, handles {hour}:{minute}:{seconds} * @param time * @return */ private double convertTime(String time) { try { return Double.parseDouble(time); } catch (NumberFormatException e) { String[] arrs = time.split(":"); double value, sum = 0; for (int i = 0; i < arrs.length; i++) { value = Double.parseDouble(arrs[arrs.length - i - 1]); sum += value * MULTIPLIER[i]; } return sum; } } }