Advertisement
Guest User

StreamController.kava

a guest
Mar 19th, 2013
60
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 17.24 KB | None | 0 0
  1. /*
  2.  This file is part of Subsonic.
  3.  
  4.  Subsonic is free software: you can redistribute it and/or modify
  5.  it under the terms of the GNU General Public License as published by
  6.  the Free Software Foundation, either version 3 of the License, or
  7.  (at your option) any later version.
  8.  
  9.  Subsonic is distributed in the hope that it will be useful,
  10.  but WITHOUT ANY WARRANTY; without even the implied warranty of
  11.  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12.  GNU General Public License for more details.
  13.  
  14.  You should have received a copy of the GNU General Public License
  15.  along with Subsonic.  If not, see <http://www.gnu.org/licenses/>.
  16.  
  17.  Copyright 2009 (C) Sindre Mehus
  18.  */
  19. package net.sourceforge.subsonic.controller;
  20.  
  21. import java.awt.Dimension;
  22. import java.io.IOException;
  23. import java.io.OutputStream;
  24. import java.util.Arrays;
  25. import java.util.regex.Matcher;
  26. import java.util.regex.Pattern;
  27.  
  28. import javax.servlet.http.HttpServletRequest;
  29. import javax.servlet.http.HttpServletResponse;
  30.  
  31. import net.sourceforge.subsonic.domain.MediaFile;
  32. import net.sourceforge.subsonic.service.MediaFileService;
  33. import net.sourceforge.subsonic.service.SearchService;
  34. import org.apache.commons.io.IOUtils;
  35. import org.apache.commons.lang.math.LongRange;
  36. import org.springframework.web.bind.ServletRequestBindingException;
  37. import org.springframework.web.bind.ServletRequestUtils;
  38. import org.springframework.web.servlet.ModelAndView;
  39. import org.springframework.web.servlet.mvc.Controller;
  40.  
  41. import net.sourceforge.subsonic.Logger;
  42. import net.sourceforge.subsonic.domain.Player;
  43. import net.sourceforge.subsonic.domain.PlayQueue;
  44. import net.sourceforge.subsonic.domain.TransferStatus;
  45. import net.sourceforge.subsonic.domain.User;
  46. import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
  47. import net.sourceforge.subsonic.io.PlayQueueInputStream;
  48. import net.sourceforge.subsonic.io.RangeOutputStream;
  49. import net.sourceforge.subsonic.io.ShoutCastOutputStream;
  50. import net.sourceforge.subsonic.service.AudioScrobblerService;
  51. import net.sourceforge.subsonic.service.PlayerService;
  52. import net.sourceforge.subsonic.service.PlaylistService;
  53. import net.sourceforge.subsonic.service.SecurityService;
  54. import net.sourceforge.subsonic.service.SettingsService;
  55. import net.sourceforge.subsonic.service.StatusService;
  56. import net.sourceforge.subsonic.service.TranscodingService;
  57. import net.sourceforge.subsonic.util.StringUtil;
  58. import net.sourceforge.subsonic.util.Util;
  59.  
  60. /**
  61.  * A controller which streams the content of a {@link net.sourceforge.subsonic.domain.PlayQueue} to a remote
  62.  * {@link Player}.
  63.  *
  64.  * @author Sindre Mehus
  65.  */
  66. public class StreamController implements Controller {
  67.  
  68.     private static final Logger LOG = Logger.getLogger(StreamController.class);
  69.  
  70.     private StatusService statusService;
  71.     private PlayerService playerService;
  72.     private PlaylistService playlistService;
  73.     private SecurityService securityService;
  74.     private SettingsService settingsService;
  75.     private TranscodingService transcodingService;
  76.     private AudioScrobblerService audioScrobblerService;
  77.     private MediaFileService mediaFileService;
  78.     private SearchService searchService;
  79.  
  80.     public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
  81.  
  82.         TransferStatus status = null;
  83.         PlayQueueInputStream in = null;
  84.         Player player = playerService.getPlayer(request, response, false, true);
  85.         User user = securityService.getUserByName(player.getUsername());
  86.  
  87.         try {
  88.  
  89.             if (!user.isStreamRole()) {
  90.                 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername());
  91.                 return null;
  92.             }
  93.  
  94.             // If "playlist" request parameter is set, this is a Podcast request. In that case, create a separate
  95.             // play queue (in order to support multiple parallel Podcast streams).
  96.             Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist");
  97.             boolean isPodcast = playlistId != null;
  98.             if (isPodcast) {
  99.                 PlayQueue playQueue = new PlayQueue();
  100.                 playQueue.addFiles(false, playlistService.getFilesInPlaylist(playlistId));
  101.                 player.setPlayQueue(playQueue);
  102.                 Util.setContentLength(response, playQueue.length());
  103.                 LOG.info("Incoming Podcast request for playlist " + playlistId);
  104.             }
  105.  
  106.             String contentType = StringUtil.getMimeType(request.getParameter("suffix"));
  107.             response.setContentType(contentType);
  108.  
  109.             String preferredTargetFormat = request.getParameter("format");
  110.             Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate");
  111.             if (Integer.valueOf(0).equals(maxBitRate)) {
  112.                 maxBitRate = null;
  113.             }
  114.  
  115.             VideoTranscodingSettings videoTranscodingSettings = null;
  116.  
  117.             // Is this a request for a single file (typically from the embedded Flash player)?
  118.             // In that case, create a separate playlist (in order to support multiple parallel streams).
  119.             // Also, enable partial download (HTTP byte range).
  120.             MediaFile file = getSingleFile(request);
  121.             boolean isSingleFile = file != null;
  122.             LongRange range = null;
  123.  
  124.             if (isSingleFile) {
  125.                 PlayQueue playQueue = new PlayQueue();
  126.                 playQueue.addFiles(true, file);
  127.                 player.setPlayQueue(playQueue);
  128.  
  129.                 if (!file.isVideo()) {
  130.                     response.setIntHeader("ETag", file.getId());
  131.                     response.setHeader("Accept-Ranges", "bytes");
  132.                 }
  133.  
  134.                 TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, null, false);
  135.                 long fileLength = getFileLength(parameters);
  136.                 boolean isConversion = parameters.isDownsample() || parameters.isTranscode();
  137.                 boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request, "estimateContentLength", false);
  138.                 boolean isHls = ServletRequestUtils.getBooleanParameter(request, "hls", false);
  139.  
  140.                 range = getRange(request, file);
  141.                 if (range != null) {
  142.                     LOG.info("Got range: " + range);
  143.                     response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
  144.                     Util.setContentLength(response, fileLength - range.getMinimumLong());
  145.                     long firstBytePos = range.getMinimumLong();
  146.                     long lastBytePos = fileLength - 1;
  147.                     response.setHeader("Content-Range", "bytes " + firstBytePos + "-" + lastBytePos + "/" + fileLength);
  148.                 } else if (!isHls && (!isConversion || estimateContentLength)) {
  149.                     Util.setContentLength(response, fileLength);
  150.                 }
  151.  
  152.                 if (isHls) {
  153.                     response.setContentType(StringUtil.getMimeType("ts")); // HLS is always MPEG TS.
  154.                 } else {
  155.                     String transcodedSuffix = transcodingService.getSuffix(player, file, preferredTargetFormat);
  156.                     response.setContentType(StringUtil.getMimeType(transcodedSuffix));
  157.                 }
  158.  
  159.                 if (file.isVideo() || isHls) {
  160.                     videoTranscodingSettings = createVideoTranscodingSettings(file, request);
  161.                 }
  162.             }
  163.  
  164.             if (request.getMethod().equals("HEAD")) {
  165.                 return null;
  166.             }
  167.  
  168.             // Terminate any other streams to this player.
  169.             if (!isPodcast && !isSingleFile) {
  170.                 for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) {
  171.                     if (streamStatus.isActive()) {
  172.                         streamStatus.terminate();
  173.                     }
  174.                 }
  175.             }
  176.  
  177.             status = statusService.createStreamStatus(player);
  178.  
  179.             in = new PlayQueueInputStream(player, status, maxBitRate, preferredTargetFormat, videoTranscodingSettings, transcodingService,
  180.                     audioScrobblerService, mediaFileService, searchService);
  181.             OutputStream out = RangeOutputStream.wrap(response.getOutputStream(), range);
  182.  
  183.             // Enabled SHOUTcast, if requested.
  184.             boolean isShoutCastRequested = "1".equals(request.getHeader("icy-metadata"));
  185.             if (isShoutCastRequested && !isSingleFile) {
  186.                 response.setHeader("icy-metaint", "" + ShoutCastOutputStream.META_DATA_INTERVAL);
  187.                 response.setHeader("icy-notice1", "This stream is served using Madsonic");
  188.                 response.setHeader("icy-notice2", "Madsonic - Free media streamer - madsonic.org");
  189.                 response.setHeader("icy-name", "Madsonic");
  190.                 response.setHeader("icy-genre", "Mixed");
  191.                 response.setHeader("icy-url", "http://madsonic.org/");
  192.                 out = new ShoutCastOutputStream(out, player.getPlayQueue(), settingsService);
  193.             }
  194.  
  195.             final int BUFFER_SIZE = 2048;
  196.             byte[] buf = new byte[BUFFER_SIZE];
  197.  
  198.             while (true) {
  199.  
  200.                 // Check if stream has been terminated.
  201.                 if (status.terminated()) {
  202.                     return null;
  203.                 }
  204.  
  205.                 if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
  206.                     if (isPodcast || isSingleFile) {
  207.                         break;
  208.                     } else {
  209.                         sendDummy(buf, out);
  210.                     }
  211.                 } else {
  212.  
  213.                     int n = in.read(buf);
  214.                     if (n == -1) {
  215.                         if (isPodcast || isSingleFile) {
  216.                             break;
  217.                         } else {
  218.                             sendDummy(buf, out);
  219.                         }
  220.                     } else {
  221.                         out.write(buf, 0, n);
  222.                     }
  223.                 }
  224.             }
  225.  
  226.         } finally {
  227.             if (status != null) {
  228.                 securityService.updateUserByteCounts(user, status.getBytesTransfered(), 0L, 0L);
  229.                 statusService.removeStreamStatus(status);
  230.             }
  231.             IOUtils.closeQuietly(in);
  232.         }
  233.         return null;
  234.     }
  235.  
  236.     private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException {
  237.         String path = request.getParameter("path");
  238.         if (path != null) {
  239.             return mediaFileService.getMediaFile(path);
  240.         }
  241.         Integer id = ServletRequestUtils.getIntParameter(request, "id");
  242.         if (id != null) {
  243.             return mediaFileService.getMediaFile(id);
  244.         }
  245.         return null;
  246.     }
  247.  
  248.     private long getFileLength(TranscodingService.Parameters parameters) {
  249.         MediaFile file = parameters.getMediaFile();
  250.  
  251.         if (!parameters.isDownsample() && !parameters.isTranscode()) {
  252.             return file.getFileSize();
  253.         }
  254.         Integer duration = file.getDurationSeconds();
  255.         Integer maxBitRate = parameters.getMaxBitRate();
  256.  
  257.         if (duration == null) {
  258.             LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size.");
  259.             return file.getFileSize();
  260.         }
  261.  
  262.         if (maxBitRate == null) {
  263.             LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size.");
  264.             return file.getFileSize();
  265.         }
  266.  
  267.         return duration * maxBitRate * 1000L / 8L;
  268.     }
  269.  
  270.     private LongRange getRange(HttpServletRequest request, MediaFile file) {
  271.  
  272.         // First, look for "Range" HTTP header.
  273.         LongRange range = StringUtil.parseRange(request.getHeader("Range"));
  274.         if (range != null) {
  275.             return range;
  276.         }
  277.  
  278.         // Second, look for "offsetSeconds" request parameter.
  279.         String offsetSeconds = request.getParameter("offsetSeconds");
  280.         range = parseAndConvertOffsetSeconds(offsetSeconds, file);
  281.         if (range != null) {
  282.             return range;
  283.         }
  284.  
  285.         return null;
  286.     }
  287.  
  288.     private LongRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) {
  289.         if (offsetSeconds == null) {
  290.             return null;
  291.         }
  292.  
  293.         try {
  294.             Integer duration = file.getDurationSeconds();
  295.             Long fileSize = file.getFileSize();
  296.             if (duration == null || fileSize == null) {
  297.                 return null;
  298.             }
  299.             float offset = Float.parseFloat(offsetSeconds);
  300.  
  301.             // Convert from time offset to byte offset.
  302.             long byteOffset = (long) (fileSize * (offset / duration));
  303.             return new LongRange(byteOffset, Long.MAX_VALUE);
  304.  
  305.         } catch (Exception x) {
  306.             LOG.error("Failed to parse and convert time offset: " + offsetSeconds, x);
  307.             return null;
  308.         }
  309.     }
  310.  
  311.     private VideoTranscodingSettings createVideoTranscodingSettings(MediaFile file, HttpServletRequest request) throws ServletRequestBindingException {
  312.         Integer existingWidth = file.getWidth();
  313.         Integer existingHeight = file.getHeight();
  314.         Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate");
  315.         int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0);
  316.         int defaultDuration = file.getDurationSeconds() == null ? Integer.MAX_VALUE : file.getDurationSeconds() - timeOffset;
  317.         int duration = ServletRequestUtils.getIntParameter(request, "duration", defaultDuration);
  318.         boolean hls = ServletRequestUtils.getBooleanParameter(request, "hls", false);
  319.  
  320.         Dimension dim = getRequestedVideoSize(request.getParameter("size"));
  321.         if (dim == null) {
  322.             dim = getSuitableVideoSize(existingWidth, existingHeight, maxBitRate);
  323.         }
  324.  
  325.         return new VideoTranscodingSettings(dim.width, dim.height, timeOffset, duration, hls);
  326.     }
  327.  
  328.     protected Dimension getRequestedVideoSize(String sizeSpec) {
  329.         if (sizeSpec == null) {
  330.             return null;
  331.         }
  332.  
  333.         Pattern pattern = Pattern.compile("^(\\d+)x(\\d+)$");
  334.         Matcher matcher = pattern.matcher(sizeSpec);
  335.         if (matcher.find()) {
  336.             int w = Integer.parseInt(matcher.group(1));
  337.             int h = Integer.parseInt(matcher.group(2));
  338.             if (w >= 0 && h >= 0 && w <= 2000 && h <= 2000) {
  339.                 return new Dimension(w, h);
  340.             }
  341.         }
  342.         return null;
  343.     }
  344.  
  345.     protected Dimension getSuitableVideoSize(Integer existingWidth, Integer existingHeight, Integer maxBitRate) {
  346.         if (maxBitRate == null) {
  347.             return new Dimension(400, 300);
  348.         }
  349.  
  350.         int w, h;
  351.         if (maxBitRate < 400) {
  352.             w = 400; h = 300;
  353.         } else if (maxBitRate < 600) {
  354.             w = 480; h = 360;
  355.         } else if (maxBitRate < 1800) {
  356.             w = 640; h = 480;
  357.         } else {
  358.             w = 960; h = 720;
  359.         }
  360.  
  361.         if (existingWidth == null || existingHeight == null) {
  362.             return new Dimension(w, h);
  363.         }
  364.  
  365.         if (existingWidth < w || existingHeight < h) {
  366.             return new Dimension(even(existingWidth), even(existingHeight));
  367.         }
  368.  
  369.         double aspectRate = existingWidth.doubleValue() / existingHeight.doubleValue();
  370.         h = (int) Math.round(w / aspectRate);
  371.  
  372.         return new Dimension(even(w), even(h));
  373.     }
  374.  
  375.     // Make sure width and height are multiples of two, as some versions of ffmpeg require it.
  376.     private int even(int size) {
  377.         return size + (size % 2);
  378.     }
  379.  
  380.     /**
  381.      * Feed the other end with some dummy data to keep it from reconnecting.
  382.      */
  383.     private void sendDummy(byte[] buf, OutputStream out) throws IOException {
  384.         try {
  385.             Thread.sleep(2000);
  386.         } catch (InterruptedException x) {
  387.             LOG.warn("Interrupted in sleep.", x);
  388.         }
  389.         Arrays.fill(buf, (byte) 0xFF);
  390.         out.write(buf);
  391.         out.flush();
  392.     }
  393.  
  394.     public void setStatusService(StatusService statusService) {
  395.         this.statusService = statusService;
  396.     }
  397.  
  398.     public void setPlayerService(PlayerService playerService) {
  399.         this.playerService = playerService;
  400.     }
  401.  
  402.     public void setPlaylistService(PlaylistService playlistService) {
  403.         this.playlistService = playlistService;
  404.     }
  405.  
  406.     public void setSecurityService(SecurityService securityService) {
  407.         this.securityService = securityService;
  408.     }
  409.  
  410.     public void setSettingsService(SettingsService settingsService) {
  411.         this.settingsService = settingsService;
  412.     }
  413.  
  414.     public void setTranscodingService(TranscodingService transcodingService) {
  415.         this.transcodingService = transcodingService;
  416.     }
  417.  
  418.     public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
  419.         this.audioScrobblerService = audioScrobblerService;
  420.     }
  421.  
  422.     public void setMediaFileService(MediaFileService mediaFileService) {
  423.         this.mediaFileService = mediaFileService;
  424.     }
  425.  
  426.     public void setSearchService(SearchService searchService) {
  427.         this.searchService = searchService;
  428.     }
  429. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement