/* This file is part of Subsonic. Subsonic is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Subsonic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Subsonic. If not, see <http://www.gnu.org/licenses/>. Copyright 2009 (C) Sindre Mehus */ package net.sourceforge.subsonic.controller; import net.sourceforge.subsonic.domain.MediaFile; import net.sourceforge.subsonic.domain.Player; import net.sourceforge.subsonic.service.MediaFileService; import net.sourceforge.subsonic.service.PlayerService; import net.sourceforge.subsonic.util.StringUtil; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** * Controller which produces the HLS (Http Live Streaming) playlist. * * @author Sindre Mehus */ public class HLSController implements Controller { private static final int SEGMENT_DURATION = 10; private PlayerService playerService; private MediaFileService mediaFileService; public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { int id = ServletRequestUtils.getIntParameter(request, "id"); MediaFile mediaFile = mediaFileService.getMediaFile(id); if (mediaFile == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Media file not found: " + id); return null; } Integer duration = mediaFile.getDurationSeconds(); if (duration == null || duration == 0) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unknown duration for media file: " + id); return null; } Player player = playerService.getPlayer(request, response); response.setContentType("application/vnd.apple.mpegurl"); response.setCharacterEncoding(StringUtil.ENCODING_UTF8); int[] bitRates = ServletRequestUtils.getIntParameters(request, "bitRate"); PrintWriter writer = response.getWriter(); if (bitRates.length > 1) { generateVariantPlaylist(id, player, bitRates, writer); } else { generateNormalPlaylist(id, player, bitRates.length == 1 ? bitRates[0] : null, duration, writer); } return null; } private void generateVariantPlaylist(int id, Player player, int[] bitRatesKbps, PrintWriter writer) { writer.println("#EXTM3U"); writer.println("#EXT-X-VERSION:1"); // writer.println("#EXT-X-TARGETDURATION:" + SEGMENT_DURATION); for (int bitRateKbps : bitRatesKbps) { writer.println("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitRateKbps * 1000L); writer.println("/hls/hls.m3u8?id=" + id + "&player=" + player.getId() + "&bitRate=" + bitRateKbps); } // writer.println("#EXT-X-ENDLIST"); } private void generateNormalPlaylist(int id, Player player, Integer bitRate, int totalDuration, PrintWriter writer) { writer.println("#EXTM3U"); writer.println("#EXT-X-VERSION:1"); writer.println("#EXT-X-TARGETDURATION:" + SEGMENT_DURATION); for (int i = 0; i < totalDuration / SEGMENT_DURATION; i++) { int offset = i * SEGMENT_DURATION; writer.println("#EXTINF:" + SEGMENT_DURATION + ","); writer.println(createStreamUrl(player, id, offset, SEGMENT_DURATION, bitRate)); } int remainder = totalDuration % SEGMENT_DURATION; if (remainder > 0) { writer.println("#EXTINF:" + remainder + ","); int offset = totalDuration - remainder; writer.println(createStreamUrl(player, id, offset, remainder, bitRate)); } writer.println("#EXT-X-ENDLIST"); } private String createStreamUrl(Player player, int id, int offset, int duration, Integer maxBitRate) { StringBuilder builder = new StringBuilder(); builder.append("/stream/stream.ts?id=").append(id).append("&hls=true&timeOffset=").append(offset).append("&player=") .append(player.getId()).append("&duration=").append(duration); if (maxBitRate != null) { builder.append("&maxBitRate=").append(maxBitRate); } return builder.toString(); } public void setMediaFileService(MediaFileService mediaFileService) { this.mediaFileService = mediaFileService; } public void setPlayerService(PlayerService playerService) { this.playerService = playerService; } }