/** Test the compatibility of the Playstation 3 with the MPG format. */
  @Test
  public void testPlaystationVideoMpgCompatibility() {
    // This test is only useful if the MediaInfo library is available
    assumeTrue(mediaInfoParserIsValid);

    RendererConfiguration conf =
        RendererConfiguration.getRendererConfigurationByName("Playstation 3");
    assertNotNull("No renderer named \"Playstation 3\" found.", conf);

    // Construct regular two channel MPG information
    DLNAMediaInfo info = new DLNAMediaInfo();
    info.setContainer("avi");
    DLNAMediaAudio audio = new DLNAMediaAudio();
    audio.setCodecA("ac3");
    audio.setNrAudioChannels(5);
    ArrayList<DLNAMediaAudio> audioCodes = new ArrayList<DLNAMediaAudio>();
    audioCodes.add(audio);
    info.setAudioCodes(audioCodes);
    info.setCodecV("mp4");
    Format format = new MPG();
    format.match("test.avi");
    assertEquals(
        "PS3 is reported to be incompatible with MPG", true, conf.isCompatible(info, format));

    // Construct MPG with wmv codec that the PS3 does not support natively
    info.setCodecV("wmv");
    assertEquals(
        "PS3 is reported to be compatible with MPG with wmv codec",
        false,
        conf.isCompatible(info, format));
  }
  /** Test the compatibility of the Playstation 3 with the MP3 format. */
  @Test
  public void testPlaystationAudioMp3Compatibility() {
    // This test is only useful if the MediaInfo library is available
    assumeTrue(mediaInfoParserIsValid);

    RendererConfiguration conf =
        RendererConfiguration.getRendererConfigurationByName("Playstation 3");
    assertNotNull("No renderer named \"Playstation 3\" found.", conf);

    // Construct regular two channel MP3 information
    DLNAMediaInfo info = new DLNAMediaInfo();
    info.setContainer("mp3");
    info.setMimeType(HTTPResource.AUDIO_MP3_TYPEMIME);
    DLNAMediaAudio audio = new DLNAMediaAudio();
    audio.setNrAudioChannels(2);
    ArrayList<DLNAMediaAudio> audioCodes = new ArrayList<DLNAMediaAudio>();
    audioCodes.add(audio);
    info.setAudioCodes(audioCodes);
    Format format = new MP3();
    format.match("test.mp3");
    assertEquals(
        "PS3 is reported to be incompatible with MP3", true, conf.isCompatible(info, format));

    // Construct five channel MP3 that the PS3 does not support natively
    audio.setNrAudioChannels(5);
    assertEquals(
        "PS3 is reported to be incompatible with MP3", false, conf.isCompatible(info, format));
  }
 /**
  * Test some basic functionality of {@link RendererConfiguration#isCompatible(DLNAMediaInfo,
  * Format)}
  */
 @Test
 public void testRendererConfigurationBasics() {
   // This test is only useful if the MediaInfo library is available
   assumeTrue(mediaInfoParserIsValid);
   RendererConfiguration conf =
       RendererConfiguration.getRendererConfigurationByName("Playstation 3");
   assertNotNull("No renderer named \"Playstation 3\" found.", conf);
   assertEquals(
       "With nothing provided isCompatible() should return false",
       false,
       conf.isCompatible(null, null));
 }
  /** Test the compatibility of the Playstation 3 with the GIF format. */
  @Test
  public void testPlaystationImageGifCompatibility() {
    // This test is only useful if the MediaInfo library is available
    assumeTrue(mediaInfoParserIsValid);

    RendererConfiguration conf =
        RendererConfiguration.getRendererConfigurationByName("Playstation 3");
    assertNotNull("No renderer named \"Playstation 3\" found.", conf);

    // Construct GIF information
    DLNAMediaInfo info = new DLNAMediaInfo();
    info.setContainer("gif");
    Format format = new GIF();
    format.match("test.gif");
    assertEquals(
        "PS3 is reported to be incompatible with GIF", true, conf.isCompatible(info, format));
  }
  private static void purgeCode(String mainClass, URL newUrl) {
    Class<?> clazz1 = null;

    for (Class<?> clazz : externalListenerClasses) {
      if (mainClass.equals(clazz.getCanonicalName())) {
        clazz1 = clazz;
        break;
      }
    }

    if (clazz1 == null) {
      return;
    }

    externalListenerClasses.remove(clazz1);
    ExternalListener remove = null;
    for (ExternalListener list : externalListeners) {
      if (list.getClass().equals(clazz1)) {
        remove = list;
        break;
      }
    }

    RendererConfiguration.resetAllRenderers();

    if (remove != null) {
      externalListeners.remove(remove);
      remove.shutdown();
      LooksFrame frame = (LooksFrame) PMS.get().getFrame();
      frame.getPt().removePlugin(remove);
    }

    for (int i = 0; i < 3; i++) {
      System.gc();
    }

    URLClassLoader cl = (URLClassLoader) clazz1.getClassLoader();
    URL[] urls = cl.getURLs();
    for (URL url : urls) {
      String mainClass1 = getMainClass(url);

      if (mainClass1 == null || !mainClass.equals(mainClass1)) {
        continue;
      }

      File f = url2file(url);
      File f1 = url2file(newUrl);

      if (f1 == null || f == null) {
        continue;
      }

      if (!f1.getName().equals(f.getName())) {
        addToPurgeFile(f);
      }
    }
  }
 public static WebRender matchRenderer(String user, HttpExchange t) {
   int browser = WebRender.getBrowser(t.getRequestHeaders().getFirst("User-agent"));
   String confName = WebRender.getBrowserName(browser);
   RendererConfiguration r =
       RendererConfiguration.find(confName, t.getRemoteAddress().getAddress());
   return ((r instanceof WebRender)
           && (StringUtils.isBlank(user) || user.equals(((WebRender) r).getUser())))
       ? (WebRender) r
       : null;
 }
  @Before
  public void setUp() {
    // Silence all log messages from the PMS code that is being tested
    LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
    context.reset();

    // Initialize the RendererConfiguration
    RendererConfiguration.loadRendererConfigurations();

    mediaInfoParserIsValid = MediaInfoParser.isValid();
  }
  /**
   * 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());
            }
          }
        }
      }

      /*
      we add (or may add) a null entry to the audio list and/or subtitle list
      to ensure the inner loop is always entered:

      for audio in audioTracks:
      for subtitle in subtitleTracks:
      for player in players:
      newResource(audio, subtitle, player)

      there are 4 different scenarios:

      1) a file with audio tracks and no subtitles (subtitle == null): in that case we want
      to assign a player for each audio track

      2) a file with subtitles and no audio tracks (audio == null): in that case we want
      to assign a player for each subtitle track

      3) a file with no audio tracks (audio == null) and no subtitles (subtitle == null)
      e.g. an audio file, a video with no sound and no subtitles or a web audio/video file:
      in that case we still want to provide a selection of players e.g. FFmpeg Web Video
      and VLC Web Video for a web video or FFmpeg Audio and MPlayer Audio for an audio file

      4) one or more audio tracks AND one or more subtitle tracks: this is the case this code
      used to handle when it solely dealt with (local) video files: assign a player
      for each combination of audio track and subtitle track

      If a null audio or subtitle track is passed to createResourceWithAudioSubtitlePlayer,
      it sets the copy's corresponding mediaAudio (AKA params.aid) or mediaSubtitle
      (AKA params.sid) value to null.

      Note: this is the only place in the codebase where mediaAudio and mediaSubtitle
      are assigned (ignoring the trivial clone operation in ChapterFileTranscodeVirtualFolder),
      so setting one or both of them to null is a no-op as they're already null.
      */

      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);
      }
    }
  }
  public void answer(OutputStream output, StartStopListenerDelegate startStopListenerDelegate)
      throws IOException {
    this.output = output;

    long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit
    if (lowRange != 0 || highRange != 0) {
      output(output, 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.
        output(output, HTTP_500);
      } else {
        output(output, http10 ? HTTP_200_OK_10 : HTTP_200_OK);
      }
    }

    StringBuilder response = new StringBuilder();
    DLNAResource dlna = null;
    boolean xbox = mediaRenderer.isXBOX();

    // 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/")) {
      output(output, "Content-Type: text/html");
      response.append(HTMLConsole.servePage(argument.substring(8)));
    } else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) {
      String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/"));
      id = id.replace("%24", "$"); // popcorn hour ?
      List<DLNAResource> files =
          PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer);
      if (transferMode != null) {
        output(output, "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(output, "Content-Type: " + dlna.getThumbnailContentType());
          output(output, "Accept-Ranges: bytes");
          output(output, "Expires: " + getFUTUREDATE() + " GMT");
          output(output, "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(output, "Content-Type: text/plain");
          output(output, "Expires: " + getFUTUREDATE() + " GMT");
          List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitleTracksList();

          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);
            if (sub.isExternal()) {
              inputStream = new java.io.FileInputStream(sub.getExternalFile());
            }
          }
        } else {
          // This is a request for a regular file.
          String name = dlna.getDisplayName(mediaRenderer);
          inputStream =
              dlna.getInputStream(
                  Range.create(lowRange, highRange, timeseek, timeRangeEnd), mediaRenderer);
          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);
            output(output, "Content-Type: " + getRendererMimeType(dlna.mimeType(), mediaRenderer));

            if (!configuration.isDisableSubtitles()) {
              // 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<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitleTracksList();

                if (subs != null && !subs.isEmpty()) {
                  DLNAMediaSubtitle sub = subs.get(0);
                  String subtitleUrl;
                  String subExtension = sub.getType().getExtension();

                  if (isNotBlank(subExtension)) {
                    subtitleUrl =
                        "http://"
                            + PMS.get().getServer().getHost()
                            + ':'
                            + PMS.get().getServer().getPort()
                            + "/get/"
                            + id
                            + "/subtitle0000."
                            + subExtension;
                  } else {
                    subtitleUrl =
                        "http://"
                            + PMS.get().getServer().getHost()
                            + ':'
                            + PMS.get().getServer().getPort()
                            + "/get/"
                            + id
                            + "/subtitle0000";
                  }

                  output(output, subtitleHttpHeader + ": " + subtitleUrl);
                }
              }
            }

            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(
                  output,
                  "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;
            }

            if (contentFeatures != null) {
              output(output, "ContentFeatures.DLNA.ORG: " + dlna.getDlnaContentFeatures());
            }

            if (dlna.getPlayer() == null || xbox) {
              output(output, "Accept-Ranges: bytes");
            }

            output(output, "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(output, "Content-Type: image/png");
      } else {
        output(output, "Content-Type: image/jpeg");
      }
      output(output, "Accept-Ranges: bytes");
      output(output, "Connection: keep-alive");
      output(output, "Expires: " + getFUTUREDATE() + " GMT");
      inputStream = getResourceInputStream(argument);
    } else if ((method.equals("GET") || method.equals("HEAD"))
        && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) {
      String profileName = configuration.getProfileName();
      output(output, CONTENT_TYPE);
      output(output, "Cache-Control: no-cache");
      output(output, "Expires: 0");
      output(output, "Accept-Ranges: bytes");
      output(output, "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]", PMS.get().usn()); // .substring(0, PMS.get().usn().length()-2));
        s = s.replace("[host]", PMS.get().getServer().getHost());
        s = s.replace("[port]", "" + PMS.get().getServer().getPort());
        if (xbox) {
          logger.debug("DLNA changes for Xbox 360");
          s =
              s.replace(
                  "PS3 Media Server",
                  "PS3 Media Server [" + profileName + "] : Windows Media Connect");
          s =
              s.replace(
                  "<modelName>PMS</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("PS3 Media Server", "PS3 Media Server [" + profileName + "]");
        }
        inputStream = new ByteArrayInputStream(s.getBytes());
      }
    } else if (method.equals("POST")
        && (argument.contains("MS_MediaReceiverRegistrar_control")
            || argument.contains("mrr/control"))) {
      output(output, 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_2);
        response.append(CRLF);
      } else if (soapaction != null && soapaction.contains("IsValidated")) {
        response.append(HTTPXMLHelper.XBOX_1);
        response.append(CRLF);
      }
      response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
      response.append(CRLF);
    } else if (method.equals("POST") && argument.endsWith("upnp/control/connection_manager")) {
      output(output, CONTENT_TYPE_UTF8);
      if (soapaction != null && 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("SUBSCRIBE")) {
      if (soapaction == null) {
        // Ignore this
        return;
      }
      output(output, CONTENT_TYPE_UTF8);
      output(output, "Content-Length: 0");
      output(output, "Connection: close");
      output(output, "SID: " + PMS.get().usn());
      output(output, "Server: " + PMS.get().getServerName());
      output(output, "Timeout: Second-1800");
      output(output, "");
      output.flush();
      // output.close();

      String cb = soapaction.replace("<", "").replace(">", "");

      try {
        URL soapActionUrl = new URL(cb);
        String addr = soapActionUrl.getHost();
        int port = soapActionUrl.getPort();
        Socket sock = new Socket(addr, port);
        OutputStream out = sock.getOutputStream();

        output(out, "NOTIFY /" + argument + " HTTP/1.1");
        output(out, "SID: " + PMS.get().usn());
        output(out, "SEQ: " + 0);
        output(out, "NT: upnp:event");
        output(out, "NTS: upnp:propchange");
        output(out, "HOST: " + addr + ":" + port);
        output(out, CONTENT_TYPE_UTF8);
        sock.close();
      } catch (MalformedURLException ex) {
        logger.debug("Cannot parse address and port from soap action \"" + soapaction + "\"", ex);
      }

      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);
      }
    } else if (method.equals("POST") && argument.endsWith("upnp/control/content_directory")) {
      output(output, CONTENT_TYPE_UTF8);

      if (soapaction != null && 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("<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.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 != null
          && soapaction.indexOf("ContentDirectory:1#X_GetFeatureList")
              > -1) { // 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.UPNP_INVALID_ACTION);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction != null
          && 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 != null
          && (soapaction.contains("ContentDirectory:1#Browse")
              || soapaction.contains("ContentDirectory:1#Search"))) {
        objectID = getEnclosingValue(content, "<ObjectID>", "</ObjectID>");
        String containerID = null;
        if (isEmpty(objectID) && xbox) {
          containerID = getEnclosingValue(content, "<ContainerID>", "</ContainerID>");
          if (containerID == null || !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 != null && 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 != null && soapaction.contains("ContentDirectory:1#Search")) {
          browseFlag = "BrowseDirectChildren";
        }

        // XBOX virtual containers ... doh
        String searchCriteria = null;
        if (xbox
            && 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 = &quot;", "&quot;)");
            if (artist != null) {
              objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
              searchCriteria = artist;
            }
          }
        }

        List<DLNAResource> 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.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();
        }

        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 startingIndex
            totalCount = startingIndex;
          }

          response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>");
        } else if (browseFlag != null && browseFlag.equals("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 != null && 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());
      }
    }

    output(output, "Server: " + PMS.get().getServerName());

    if (response.length() > 0) {
      byte responseData[] = response.toString().getBytes("UTF-8");
      output(output, "Content-Length: " + responseData.length);
      output(output, "");

      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.
          output(output, "Content-Length: " + CLoverride);
        }
      } else {
        int cl = inputStream.available();
        logger.trace("Available Content-Length: " + cl);
        output(output, "Content-Length: " + cl);
      }

      if (timeseek > 0 && dlna != null) {
        String timeseekValue = DLNAMediaInfo.getDurationString(timeseek);
        String timetotalValue = dlna.getMedia().getDurationString();
        output(
            output,
            "TimeSeekRange.dlna.org: npt="
                + timeseekValue
                + "-"
                + timetotalValue
                + "/"
                + timetotalValue);
        output(
            output,
            "X-Seek-Range: npt=" + timeseekValue + "-" + timetotalValue + "/" + timetotalValue);
      }

      output(output, "");
      int sendB = 0;

      if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) {
        sendB =
            sendBytes(inputStream); // , ((lowRange > 0 && highRange > 0)?(highRange-lowRange):-1)
      }

      logger.trace("Sending stream: " + sendB + " bytes of " + argument);
      PMS.get().getFrame().setStatusLine(null);
    } else { // inputStream is null
      if (lowRange > 0 && highRange > 0) {
        output(output, "Content-Length: " + (highRange - lowRange + 1));
      } else {
        output(output, "Content-Length: 0");
      }

      output(output, "");
    }
  }
  public RootFolder getRoot(String user, boolean create, HttpExchange t) {
    String groupTag = getTag(user);
    String cookie = RemoteUtil.getCookie("UMS", t);
    RootFolder root;
    synchronized (roots) {
      root = roots.get(cookie);
      if (root == null) {
        // Double-check for cookie errors
        WebRender valid = RemoteUtil.matchRenderer(user, t);
        if (valid != null) {
          // A browser of the same type and user is already connected at
          // this ip but for some reason we didn't get a cookie match.
          RootFolder validRoot = valid.getRootFolder();
          // Do a reverse lookup to see if it's been registered
          for (Map.Entry<String, RootFolder> entry : roots.entrySet()) {
            if (entry.getValue() == validRoot) {
              // Found
              root = validRoot;
              cookie = entry.getKey();
              LOGGER.debug(
                  "Allowing browser connection without cookie match: {}: {}",
                  valid.getRendererName(),
                  t.getRemoteAddress().getAddress());
              break;
            }
          }
        }
      }

      if (!create || (root != null)) {
        t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/");
        return root;
      }

      ArrayList<String> tag = new ArrayList<>();
      tag.add(user);
      if (!groupTag.equals(user)) {
        tag.add(groupTag);
      }

      tag.add(t.getRemoteAddress().getHostString());
      tag.add("web");
      root = new RootFolder(tag);
      try {
        WebRender render = new WebRender(user);
        root.setDefaultRenderer(render);
        render.setRootFolder(root);
        render.associateIP(t.getRemoteAddress().getAddress());
        render.associatePort(t.getRemoteAddress().getPort());
        if (configuration.useWebSubLang()) {
          render.setSubLang(StringUtils.join(RemoteUtil.getLangs(t), ","));
        }
        //				render.setUA(t.getRequestHeaders().getFirst("User-agent"));
        render.setBrowserInfo(
            RemoteUtil.getCookie("UMSINFO", t), t.getRequestHeaders().getFirst("User-agent"));
        PMS.get().setRendererFound(render);
      } catch (ConfigurationException e) {
        root.setDefaultRenderer(RendererConfiguration.getDefaultConf());
      }
      // root.setDefaultRenderer(RendererConfiguration.getRendererConfigurationByName("web"));
      root.discoverChildren();
      cookie = UUID.randomUUID().toString();
      t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/");
      roots.put(cookie, root);
    }
    return root;
  }
  @Override
  public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    RequestV2 request = null;
    RendererConfiguration renderer = null;
    String userAgentString = null;
    StringBuilder unknownHeaders = new StringBuilder();
    String separator = "";
    boolean isWindowsMediaPlayer = false;

    HttpRequest nettyRequest = this.nettyRequest = (HttpRequest) e.getMessage();

    InetSocketAddress remoteAddress = (InetSocketAddress) e.getChannel().getRemoteAddress();
    InetAddress ia = remoteAddress.getAddress();

    // Apply the IP filter
    if (filterIp(ia)) {
      e.getChannel().close();
      LOGGER.trace("Access denied for address " + ia + " based on IP filter");
      return;
    }

    LOGGER.trace("Opened request handler on socket " + remoteAddress);
    PMS.get().getRegistry().disableGoToSleep();

    if (HttpMethod.GET.equals(nettyRequest.getMethod())) {
      request = new RequestV2("GET", nettyRequest.getUri().substring(1));
    } else if (HttpMethod.POST.equals(nettyRequest.getMethod())) {
      request = new RequestV2("POST", nettyRequest.getUri().substring(1));
    } else if (HttpMethod.HEAD.equals(nettyRequest.getMethod())) {
      request = new RequestV2("HEAD", nettyRequest.getUri().substring(1));
    } else {
      request =
          new RequestV2(nettyRequest.getMethod().getName(), nettyRequest.getUri().substring(1));
    }

    LOGGER.trace(
        "Request: "
            + nettyRequest.getProtocolVersion().getText()
            + " : "
            + request.getMethod()
            + " : "
            + request.getArgument());

    if (nettyRequest.getProtocolVersion().getMinorVersion() == 0) {
      request.setHttp10(true);
    }

    // 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 (renderer != null) {
      if (!"WMP".equals(renderer.getRendererName())) {
        PMS.get().setRendererfound(renderer);
        request.setMediaRenderer(renderer);
        LOGGER.trace(
            "Matched media renderer \"" + renderer.getRendererName() + "\" based on address " + ia);
      } else {
        LOGGER.trace("Detected and blocked Windows Media Player");
        isWindowsMediaPlayer = true;
      }
    }

    for (String name : nettyRequest.getHeaderNames()) {
      String headerLine = name + ": " + nettyRequest.getHeader(name);
      LOGGER.trace("Received on socket: " + headerLine);

      if (renderer == null
          && headerLine != null
          && headerLine.toUpperCase().startsWith("USER-AGENT")) {
        userAgentString = headerLine.substring(headerLine.indexOf(":") + 1).trim();

        // Attempt 2: try to recognize the renderer by matching the "User-Agent" header
        renderer = RendererConfiguration.getRendererConfigurationByUA(userAgentString);

        if (renderer != null) {
          if (!"WMP".equals(renderer.getRendererName())) {
            request.setMediaRenderer(renderer);
            renderer.associateIP(ia); // Associate IP address for later requests
            PMS.get().setRendererfound(renderer);
            LOGGER.trace(
                "Matched media renderer \""
                    + renderer.getRendererName()
                    + "\" based on header \""
                    + headerLine
                    + "\"");
          } else if (!isWindowsMediaPlayer) {
            LOGGER.trace("Detected and blocked Windows Media Player");
            isWindowsMediaPlayer = true;
          }
        }
      }

      if (renderer == null && headerLine != null) {
        // Attempt 3: try to recognize the renderer by matching an additional header
        renderer = RendererConfiguration.getRendererConfigurationByUAAHH(headerLine);

        if (renderer != null) {
          request.setMediaRenderer(renderer);
          renderer.associateIP(ia); // Associate IP address for later requests
          PMS.get().setRendererfound(renderer);
          LOGGER.trace(
              "Matched media renderer \""
                  + renderer.getRendererName()
                  + "\" based on header \""
                  + headerLine
                  + "\"");
        }
      }

      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().indexOf("RANGE: BYTES=") > -1) {
          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().indexOf("transfermode.dlna.org:") > -1) {
          request.setTransferMode(
              headerLine
                  .substring(headerLine.toLowerCase().indexOf("transfermode.dlna.org:") + 22)
                  .trim());
        } else if (headerLine.toLowerCase().indexOf("getcontentfeatures.dlna.org:") > -1) {
          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 possible known headers.
            for (String knownHeaderString : KNOWN_HEADERS) {
              if (headerLine.toLowerCase().startsWith(knownHeaderString.toLowerCase())) {
                isKnown = true;
                break;
              }
            }

            if (!isKnown) {
              // Truly unknown header, therefore interesting. Save for later use.
              unknownHeaders.append(separator).append(headerLine);
              separator = ", ";
            }
          }
        }
      } catch (Exception ee) {
        LOGGER.error("Error parsing HTTP headers", ee);
      }
    }

    if (!isWindowsMediaPlayer) {
      if (request != null) {
        // Still no media renderer recognized?
        if (request.getMediaRenderer() == null) {

          // Attempt 4: Not really an attempt; all other attempts to recognize
          // the renderer have failed. The only option left is to assume the
          // default renderer.
          request.setMediaRenderer(RendererConfiguration.getDefaultConf());
          LOGGER.trace(
              "Using default media renderer: " + request.getMediaRenderer().getRendererName());

          if (userAgentString != null && !userAgentString.equals("FDSSDP")) {
            // We have found an unknown renderer
            LOGGER.info(
                "Media renderer was not recognized. Possible identifying HTTP headers: User-Agent: "
                    + userAgentString
                    + ("".equals(unknownHeaders.toString())
                        ? ""
                        : ", " + unknownHeaders.toString()));
            PMS.get().setRendererfound(request.getMediaRenderer());
          }
        } else {
          if (userAgentString != null) {
            LOGGER.debug("HTTP User-Agent: " + userAgentString);
          }

          LOGGER.trace(
              "Recognized media renderer: " + request.getMediaRenderer().getRendererName());
        }
      }

      if (HttpHeaders.getContentLength(nettyRequest) > 0) {
        byte data[] = new byte[(int) HttpHeaders.getContentLength(nettyRequest)];
        ChannelBuffer content = nettyRequest.getContent();
        content.readBytes(data);
        request.setTextContent(new String(data, "UTF-8"));
      }

      if (request != null) {
        LOGGER.trace(
            "HTTP: "
                + request.getArgument()
                + " / "
                + request.getLowRange()
                + "-"
                + request.getHighRange());
      }

      writeResponse(e, request, ia);
    }
  }
  /**
   * 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 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();

    // 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 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<DLNAResource> 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, dlna.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<DLNAMediaSubtitle> 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<DLNAMediaSubtitle> 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(dlna.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", dlna.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]", PMS.get().usn()); // .substring(0, PMS.get().usn().length()-2));
        String profileName = PMS.getConfiguration().getProfileName();
        if (PMS.get().getServer().getHost() != null) {
          s = s.replace("[host]", PMS.get().getServer().getHost());
          s = s.replace("[port]", "" + PMS.get().getServer().getPort());
        }
        if (xbox) {
          LOGGER.debug("DLNA changes for Xbox 360");
          s =
              s.replace(
                  "Universal Media Server",
                  "Universal Media Server [" + profileName + "] : 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", "Universal 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("<mimetype>image/png</mimetype>", "<mimetype>image/jpeg</mimetype>");
          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.endsWith("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.endsWith("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("<Id>" + DLNAResource.getSystemUpdateId() + "</Id>");
        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#X_GetFeatureList")
          > -1) { // 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.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, "<ObjectID>", "</ObjectID>");
        String containerID = null;
        if ((objectID == null || objectID.length() == 0) && xbox) {
          containerID = getEnclosingValue(content, "<ContainerID>", "</ContainerID>");
          if (!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);

        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 = &quot;", "&quot;)");
            if (artist != null) {
              objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
              searchCriteria = artist;
            }
          }
        }

        List<DLNAResource> 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("<NumberReturned>").append(filessize - minus).append("</NumberReturned>");
        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("<TotalMatches>").append(totalCount).append("</TotalMatches>");
        } else if (browseFlag != null && browseFlag.equals("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);
        // LOGGER.trace(response.toString());
      }
    } else if (method.equals("SUBSCRIBE")) {
      output.setHeader("SID", PMS.get().usn());
      output.setHeader("TIMEOUT", "Second-1800");
      String cb = soapaction.replace("<", "").replace(">", "");
      String faddr = cb.replace("http://", "").replace("/", "");
      String addr = faddr.split(":")[0];
      int port = Integer.parseInt(faddr.split(":")[1]);
      Socket sock = new Socket(addr, port);
      OutputStream out = sock.getOutputStream();
      out.write(("NOTIFY /" + argument + " HTTP/1.1").getBytes());
      out.write(CRLF.getBytes());
      out.write(("SID: " + PMS.get().usn()).getBytes());
      out.write(CRLF.getBytes());
      out.write(("SEQ: " + 0).getBytes());
      out.write(CRLF.getBytes());
      out.write(("NT: upnp:event").getBytes());
      out.write(CRLF.getBytes());
      out.write(("NTS: upnp:propchange").getBytes());
      out.write(CRLF.getBytes());
      out.write(("HOST: " + faddr).getBytes());
      out.write(CRLF.getBytes());
      out.flush();
      out.close();
      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);
      }
    } 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("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
      response.append("<e:property>");
      response.append("<TransferIDs></TransferIDs>");
      response.append("</e:property>");
      response.append("<e:property>");
      response.append("<ContainerUpdateIDs></ContainerUpdateIDs>");
      response.append("</e:property>");
      response.append("<e:property>");
      response
          .append("<SystemUpdateID>")
          .append(DLNAResource.getSystemUpdateId())
          .append("</SystemUpdateID>");
      response.append("</e:property>");
      response.append("</e:propertyset>");
    }

    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) {
                  LOGGER.debug("Caught exception", 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) {
          LOGGER.debug("Caught exception", 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<String> it = output.getHeaderNames().iterator();

    while (it.hasNext()) {
      String headerName = it.next();
      LOGGER.trace("Sent to socket: " + headerName + ": " + output.getHeader(headerName));
    }

    return future;
  }
 /**
  * Returns the supplied MIME type customized for the supplied media renderer according to the
  * renderer's aliasing rules.
  *
  * @param mimetype MIME type to customize.
  * @param renderer media renderer to customize the MIME type for.
  * @return The MIME type
  */
 public String getRendererMimeType(String mimetype, RendererConfiguration renderer) {
   return renderer.getMimeType(mimetype);
 }
  /**
   * Test the backwards compatibility of {@link Format#isCompatible(DLNAMediaInfo,
   * RendererConfiguration)} and {@link Format#ps3compatible()}.
   */
  @SuppressWarnings("deprecation")
  @Test
  public void testBackwardsCompatibility() {
    // This test is only useful if the MediaInfo library is available
    assumeTrue(mediaInfoParserIsValid);

    // Testing ps3compatible(), so use renderer Playstation 3
    RendererConfiguration conf =
        RendererConfiguration.getRendererConfigurationByName("Playstation 3");
    assertNotNull("No renderer named \"Playstation 3\" found.", conf);

    // DVRMS: false
    DLNAMediaInfo info = new DLNAMediaInfo();
    info.setContainer("dvr");
    Format format = new DVRMS();
    format.match("test.dvr");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for DVRMS",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // ISO: false
    info = new DLNAMediaInfo();
    info.setContainer("iso");
    format = new ISO();
    format.match("test.iso");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for ISO",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // JPG: true
    info = new DLNAMediaInfo();
    info.setContainer("jpg");
    format = new JPG();
    format.match("test.jpeg");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for JPG",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // M4A: false
    info = new DLNAMediaInfo();
    info.setContainer("m4a");
    format = new M4A();
    format.match("test.m4a");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for M4A",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // MKV: false
    info = new DLNAMediaInfo();
    info.setContainer("mkv");
    format = new MKV();
    format.match("test.mkv");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for MKV",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // MP3: true
    info = new DLNAMediaInfo();
    info.setContainer("mp3");
    format = new MP3();
    format.match("test.mp3");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for MP3",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // MPG: true
    info = new DLNAMediaInfo();
    info.setContainer("avi");
    format = new MPG();
    format.match("test.mpg");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for MPG",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // OGG: false
    info = new DLNAMediaInfo();
    info.setContainer("ogg");
    format = new OGG();
    format.match("test.ogg");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for OGG",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // RAW: false
    info = new DLNAMediaInfo();
    info.setContainer("raw");
    format = new RAW();
    format.match("test.arw");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for RAW",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // WAV: true
    info = new DLNAMediaInfo();
    info.setContainer("wav");
    format = new WAV();
    format.match("test.wav");
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for WAV",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // WEB: type=IMAGE
    info = new DLNAMediaInfo();
    info.setContainer("jpg");
    format = new WEB();
    format.match("http://test.org/");
    format.setType(Format.IMAGE);
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for WEB image",
        format.ps3compatible(),
        conf.isCompatible(info, format));

    // WEB: type=VIDEO
    info = new DLNAMediaInfo();
    info.setContainer("avi");
    format.setType(Format.VIDEO);
    assertEquals(
        "isCompatible() reporting different outcome than ps3compatible() for WEB video",
        format.ps3compatible(),
        conf.isCompatible(info, format));
  }
 @Override
 public boolean isPlayerCompatible(RendererConfiguration mediaRenderer) {
   return mediaRenderer.isTranscodeToMPEGPSAC3();
 }
  @Override
  public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    RequestV2 request = null;
    RendererConfiguration renderer = null;
    String userAgentString = null;
    StringBuilder unknownHeaders = new StringBuilder();
    String separator = "";

    HttpRequest nettyRequest = this.nettyRequest = (HttpRequest) e.getMessage();

    InetSocketAddress remoteAddress = (InetSocketAddress) e.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)) {
      e.getChannel().close();
      LOGGER.trace(
          isSelf
              ? ("Ignoring self-originating request from " + ia + ":" + remoteAddress.getPort())
              : ("Access denied for address " + ia + " based on IP filter"));
      return;
    }

    LOGGER.trace("Opened request handler on socket " + remoteAddress);
    PMS.get().getRegistry().disableGoToSleep();
    request = new RequestV2(nettyRequest.getMethod().getName(), nettyRequest.getUri().substring(1));
    LOGGER.trace(
        "Request: "
            + nettyRequest.getProtocolVersion().getText()
            + " : "
            + request.getMethod()
            + " : "
            + request.getArgument());

    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);

    // 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.
    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);
      LOGGER.trace("Received on socket: " + headerLine);

      if (headerLine.toUpperCase().startsWith("USER-AGENT")) {
        userAgentString = headerLine.substring(headerLine.indexOf(':') + 1).trim();
      }

      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 (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.
              unknownHeaders.append(separator).append(headerLine);
              separator = ", ";
            }
          }
        }
      } catch (Exception ee) {
        LOGGER.error("Error parsing HTTP headers", ee);
      }
    }

    // Still no media renderer recognized?
    if (request.getMediaRenderer() == 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.
      request.setMediaRenderer(RendererConfiguration.resolve(ia, null));
      if (request.getMediaRenderer() != null) {
        LOGGER.trace(
            "Using default media renderer: " + request.getMediaRenderer().getRendererName());

        if (userAgentString != null && !userAgentString.equals("FDSSDP")) {
          // We have found an unknown renderer
          LOGGER.info(
              "Media renderer was not recognized. Possible identifying HTTP headers: User-Agent: "
                  + userAgentString
                  + ("".equals(unknownHeaders.toString()) ? "" : ", " + unknownHeaders.toString()));
          PMS.get().setRendererFound(request.getMediaRenderer());
        }
      } else {
        // If RendererConfiguration.resolve() didn't return the default renderer
        // it means we know via upnp that it's not really a renderer.
        return;
      }
    } else {
      if (userAgentString != null) {
        LOGGER.debug("HTTP User-Agent: " + userAgentString);
      }

      LOGGER.trace("Recognized media renderer: " + request.getMediaRenderer().getRendererName());
    }

    if (nettyRequest.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
      byte data[] = new byte[(int) HttpHeaders.getContentLength(nettyRequest)];
      ChannelBuffer content = nettyRequest.getContent();
      content.readBytes(data);
      request.setTextContent(new String(data, "UTF-8"));
    }

    LOGGER.trace(
        "HTTP: "
            + request.getArgument()
            + " / "
            + request.getLowRange()
            + "-"
            + request.getHighRange());

    writeResponse(ctx, e, request, ia);
  }