/* * The MIT License * * Copyright 2017 Isaac Aymerich <[email protected]>. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.github.segator.proxylive.controller; import com.github.segator.proxylive.ProxyLiveConstants; import com.github.segator.proxylive.ProxyLiveUtils; import com.github.segator.proxylive.config.FFMpegProfile; import com.github.segator.proxylive.entity.AuthToken; import com.github.segator.proxylive.entity.Channel; import com.github.segator.proxylive.entity.ClientInfo; import com.github.segator.proxylive.processor.DirectHLSTranscoderStreamProcessor; import com.github.segator.proxylive.processor.IStreamMultiplexerProcessor; import com.github.segator.proxylive.profiler.FFmpegProfilerService; import com.github.segator.proxylive.service.ChannelService; import com.github.segator.proxylive.service.EPGService; import com.github.segator.proxylive.service.TokensService; import com.github.segator.proxylive.tasks.StreamProcessorsSession; import java.io.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.github.segator.proxylive.config.ProxyLiveConfiguration; import com.github.segator.proxylive.processor.IHLSStreamProcessor; import com.github.segator.proxylive.service.AuthenticationService; import java.net.*; import java.util.*; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList; import org.json.simple.parser.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.UriComponentsBuilder; /** * * @author Isaac Aymerich <[email protected]> */ @Controller public class StreamController { private final Logger logger = LoggerFactory.getLogger(StreamController.class); @Autowired private ApplicationContext context; @Autowired private ProxyLiveConfiguration config; @Autowired private AuthenticationService authService; @Autowired private ChannelService channelService; @Autowired private EPGService epgService; @Autowired FFmpegProfilerService ffmpegProfileService; @Autowired private TokensService tokenService; @Autowired private StreamProcessorsSession streamProcessorsSession; private boolean authenticate(HttpServletRequest request, HttpServletResponse response) throws Exception { MultiValueMap<String, String> parameters = UriComponentsBuilder.fromUriString(ProxyLiveUtils.getURL(request)).build().getQueryParams(); String token = parameters.getFirst("token"); String username = parameters.getFirst("user"); if(token!=null){ return validateToken(token); }else{ return validateUser(username,parameters.getFirst("pass")); } } private boolean validateUser(String username, String pass) throws Exception { Boolean userLoggedResult = streamProcessorsSession.isUserLogged(username); if(userLoggedResult==null) { if (!authService.loginUser(username, pass)) { streamProcessorsSession.addCacheClient(username,false); userLoggedResult=false; } else { streamProcessorsSession.addCacheClient(username,true); userLoggedResult=true; } } return userLoggedResult; } private boolean validateToken(String token) { AuthToken authToken = tokenService.getTokenByID(token); if(authToken==null){ return false; } if(authToken.getExpirationDate()!=null){ return authToken.getExpirationDate().getTime()>new Date().getTime(); }else{ return true; } } @RequestMapping(value = "/view/{profile}/{channelID}",method=RequestMethod.GET) public void dispatchStream(@PathVariable("profile") String profile, @PathVariable("channelID") String channelID, HttpServletRequest request, HttpServletResponse response) throws Exception { if(!authenticate(request,response)){ response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } if(channelID.contains("?")){ channelID=channelID.split(Pattern.quote("?"))[0]; } Channel channel = channelService.getChannelByID(channelID); FFMpegProfile ffmpegProfile = ffmpegProfileService.getProfile(profile); if(channel==null || (ffmpegProfile==null && !"raw".equals(profile))){ response.setStatus(404); return; } IStreamMultiplexerProcessor iStreamProcessor = (IStreamMultiplexerProcessor) context.getBean("StreamProcessor", ProxyLiveConstants.STREAM_MODE, channel.getName(), channel, profile); ClientInfo client = streamProcessorsSession.manage(iStreamProcessor, request); logger.debug("Open Stream " + channelID + " by " + client.getClientUser()); iStreamProcessor.start(); if (iStreamProcessor.isConnected()) { response.setHeader("Connection", "close"); response.setHeader("Content-Type", "video/mpeg"); response.setStatus(HttpStatus.OK.value()); OutputStream clientStream = response.getOutputStream(); if(!clientStream.getClass().getName().equals("javax.servlet.http.NoBodyOutputStream")) { InputStream multiplexedInputStream = iStreamProcessor.getMultiplexedInputStream(); byte[] buffer = new byte[config.getBuffers().getChunkSize()]; int len; try { //RandomAccessFile fis = new RandomAccessFile("C:\\lol\\video.ts","r"); long lastReaded = new Date().getTime(); while (true) { len = multiplexedInputStream.read(buffer); if (len > 0) { lastReaded = new Date().getTime(); clientStream.write(buffer, 0, len); } else { if(!iStreamProcessor.isConnected()){ throw new IOException("Disconnected" + client + " because task crashed on " + iStreamProcessor); }else if((new Date().getTime() - lastReaded) > config.getStreamTimeoutMilis()) { throw new IOException("Disconnected" + client + " because timeout on " + iStreamProcessor); /*}else if((new Date().getTime() - lastReaded) > 500 && profile.equals("raw")) { if(fis.getFilePointer()==fis.length()){fis.seek(0);} len = fis.read(buffer); clientStream.write(buffer, 0, len); Thread.sleep(100);*/ }else { Thread.sleep(10); } } } } catch (Exception ex) { try { clientStream.close(); } catch (Exception ex2) { } } } iStreamProcessor.stop(false); streamProcessorsSession.removeClientInfo(client, iStreamProcessor); } else { iStreamProcessor.stop(false); streamProcessorsSession.removeClientInfo(client, iStreamProcessor); response.setStatus(HttpStatus.NOT_FOUND.value()); } logger.debug("Close Stream " + channelID + " by " + client.getClientUser()); } @RequestMapping(value = "/crossdomain.xml", method = RequestMethod.GET) public @ResponseBody String getCrossDomain() { return "<?xml version=\"1.0\" ?>\n" + "<cross-domain-policy>\n" + "<allow-access-from domain=\"*\" />\n" + "</cross-domain-policy>"; } @RequestMapping(value = "epg", method = {RequestMethod.GET,RequestMethod.HEAD}) public ResponseEntity<Resource> readEPG(HttpServletRequest request) throws IOException { String fileName="xmltv.xml"; /*Enumeration headerNames = request.getHeaderNames(); while(headerNames.hasMoreElements()) { String headerName = (String)headerNames.nextElement(); System.out.println("" + headerName + ":" + request.getHeader(headerName)); }*/ File epgFile = epgService.getEPG(); Resource resource = new UrlResource(epgFile.toPath().toUri()); // Try to determine file's content type String contentType = null; contentType = request.getServletContext().getMimeType(fileName); // Fallback to the default content type if type could not be determined if(contentType == null) { contentType = "application/octet-stream"; } return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .contentLength(epgFile.length()) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") .header("Cache-Control", "no-cache, no-store, must-revalidate") .header("Pragma", "no-cache") .header("Expires", "0") .lastModified(epgFile.lastModified()) .body(resource); } @RequestMapping(value = "channel/list/{format:^mpeg|hls$}/{profile}", method = RequestMethod.GET) public @ResponseBody String generatePlaylist(HttpServletRequest request, HttpServletResponse response, @PathVariable("profile") String profile, @PathVariable("format") String format) throws MalformedURLException, ProtocolException, IOException, ParseException, Exception { MultiValueMap<String, String> parameters = UriComponentsBuilder.fromUriString(ProxyLiveUtils.getURL(request)).build().getQueryParams(); if(!authenticate(request,response)){ response.setStatus(HttpStatus.UNAUTHORIZED.value()); return "invalid user"; } FFMpegProfile ffmpegProfile = ffmpegProfileService.getProfile(profile); if(ffmpegProfile==null && !"raw".equals(profile)){ response.setStatus(404); return "Profile not found"; } response.setHeader("Content-Disposition", "attachment; filename=playlist.m3u"); response.setHeader("Access-Control-Allow-Origin", "*"); response.setStatus(HttpStatus.OK.value()); StringBuffer buffer = new StringBuffer(); String requestBaseURL = ProxyLiveUtils.getBaseURL(request); List<Channel> channelsOrdered = new ArrayList(channelService.getChannelList()); channelsOrdered.sort(new Comparator<Channel>() { @Override public int compare(Channel o1, Channel o2) { return o1.getNumber().compareTo(o2.getNumber()); } }); String EPGURL = String.format("%s/epg", requestBaseURL); buffer.append(String.format("#EXTM3U cache=2000 url-tvg=\"%s\" x-tvg-url=\"%s\" tvg-shift=0\r\n\r\n",EPGURL,EPGURL)); for (Channel channel : channelsOrdered) { Set<String> categories= new HashSet<>(); for (String channelCategory: channel.getCategories()) { categories.add(String.format(" group-title=\"%s\" ",channelCategory)); } String categoriesString = String.join(" ",categories); String epgIDString = ""; if(channel.getEpgID()!=null){ epgIDString = String.format("tvg-id=\"%s\"",channel.getEpgID()); } String logoURL=""; if(channel.getLogoURL()!=null || channel.getLogoFile()!=null){ logoURL = String.format("tvg-logo=\"%s/channel/%s/icon\"",requestBaseURL,channel.getId()); } String channelURL=String.format("%s/view/%s/%s", requestBaseURL, profile, channel.getId()); if(format.equals("hls")){ channelURL=channelURL+"/playlist.m3u8"; } channelURL=String.format("%s?user=%s&pass=%s",channelURL,parameters.getFirst("user"),parameters.getFirst("pass")); buffer.append(String.format("#EXTINF:-1 tvg-chno=\"%d\" %s %s %s tvg-name=\"%s\" type=\"%s\",%s\r\n%s\r\n", channel.getNumber(),logoURL,categoriesString,epgIDString,channel.getName(),format,channel.getName(),channelURL)); } return buffer.toString(); } @RequestMapping(value = "channel/{channelID}/icon", method = {RequestMethod.GET,RequestMethod.HEAD}) public ResponseEntity<Resource> downloadIcon(HttpServletRequest request,@PathVariable("channelID") String channelID) throws Exception { Channel channel = channelService.getChannelByID(channelID); if(channel!=null && channel.getLogoFile()!=null){ Resource resource = new UrlResource(channel.getLogoFile().toPath().toUri()); String contentType = null; contentType = request.getServletContext().getMimeType(channel.getLogoFile().getName()); // Fallback to the default content type if type could not be determined if(contentType == null) { contentType = "application/octet-stream"; } return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .contentLength(channel.getLogoFile().length()) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + channel.getLogoFile().getName() + "\"") .lastModified(channel.getLogoFile().lastModified()) .body(resource); }else{ return ResponseEntity.notFound().build(); } } @RequestMapping(value = "/view/{profile}/{channelID}/{file:^(?i)playlist.m3u8|dummy\\d*.ts|playlist\\d*.ts$}", method = RequestMethod.GET) public void dispatchHLS(@PathVariable("profile") String profile, @PathVariable("channelID") String channelID, @PathVariable String file, HttpServletRequest request, HttpServletResponse response) throws Exception { long now = new Date().getTime(); if(!authenticate(request,response)){ response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } if(!config.getFfmpeg().getHls().getEnabled()){ response.setStatus(404); return; } String clientIdentifier = ProxyLiveUtils.getRequestIP(request) + ProxyLiveUtils.getBrowserInfo(request); Channel channel = channelService.getChannelByID(channelID); FFMpegProfile ffmpegProfile = ffmpegProfileService.getProfile(profile); if(channel==null || (ffmpegProfile==null && !"raw".equals(profile))){ response.setStatus(404); return; } DirectHLSTranscoderStreamProcessor hlsStreamProcessor = streamProcessorsSession.getHLSStream(ProxyLiveUtils.getRequestIP(request), channel.getId(), profile); if (hlsStreamProcessor == null) { hlsStreamProcessor = (DirectHLSTranscoderStreamProcessor) context.getBean("StreamProcessor", ProxyLiveConstants.HLS_MODE, clientIdentifier, channel, profile); hlsStreamProcessor.start(); } ClientInfo client = streamProcessorsSession.manage(hlsStreamProcessor, request); //if (hlsStreamProcessor.isConnected()) { file = file.toLowerCase(); logger.debug("Client require:" + file + " after " + (new Date().getTime() - now)/1000); InputStream downloadFile = getFileToUpload(file, hlsStreamProcessor, request,response); logger.debug("Client get Stream:" + file + " after " + (new Date().getTime() - now)/1000); //probably the stream is not ready yet. //file.endsWith("m3u8") && /*if(downloadFile==null){ //send fake playlist to stream something meanwhile is waitting uploadFakeHLSFile(file,request,response); System.out.println("Client dowloaded:" + file + " after " + (new Date().getTime() - now)/1000); return; }*/ if (downloadFile != null) { response.setStatus(200); //response.setHeader(file, file); byte[] buffer = new byte[config.getBuffers().getChunkSize()]; OutputStream output = response.getOutputStream(); int len = 0; try { while ((len = downloadFile.read(buffer)) != -1) { output.write(buffer, 0, len); } } catch (Exception ex) { } finally { try { output.close(); } catch (Exception ex2) { } try { downloadFile.close(); } catch (Exception ex2) { } } } else { response.setStatus(404); } logger.debug("Client exit("+response.getStatus()+") request:" + file + " after " + (new Date().getTime() - now)/1000); //} else { // response.setStatus(404); //} } @RequestMapping(value="/hls/dummy/{fileName:^(?i)playlist.m3u8|dummy\\d*.ts$}",method=RequestMethod.GET) private void uploadFakeHLSFile(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException { File file = new File("C:\\lol\\"+fileName); long now = new Date().getTime(); while(!file.exists() && (new Date().getTime() - now) < config.getFfmpeg().getHls().getTimeout()*1000) { Thread.sleep(100); } FileInputStream fis=new FileInputStream(file); response.setHeader("Connection", "close");//keep-alive response.setHeader("Access-Control-Allow-Origin","*"); response.setHeader("Access-Control-Expose-Headers","Content-Length"); response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); response.setHeader(HttpHeaders.CONTENT_LENGTH, ""+file.length()); response.setHeader(HttpHeaders.CONTENT_TYPE, getMediaType(fileName).toString()); response.setStatus(200); //response.setHeader(file, file); byte[] buffer = new byte[config.getBuffers().getChunkSize()]; OutputStream output = response.getOutputStream(); int len = 0; try { while ((len = fis.read(buffer)) != -1) { output.write(buffer, 0, len); } } catch (Exception ex) { } finally { try { output.close(); } catch (Exception ex2) { } try { fis.close(); } catch (Exception ex2) { } } } private MediaType getMediaType(String downloadFile) { String name = downloadFile; try { String extension = name.substring(name.lastIndexOf(".") + 1); switch (extension.toLowerCase()) { case "ts": return new MediaType("video", "mp2t"); case "m3u8": return new MediaType("application", "vnd.apple.mpegurl"); //x-mpegURL: case "mpd": return new MediaType("application","dash+xml"); } } catch (Exception e) { logger.error("Error",e); } return null; } private InputStream getFileToUpload(String file, IHLSStreamProcessor hlsStreamProcessor, HttpServletRequest request,HttpServletResponse respHeaders) throws IOException, URISyntaxException, InterruptedException { InputStream downloadFile = null; long now = new Date().getTime(); Long fileSize = null; if (file.endsWith("m3u8")) { do { downloadFile = hlsStreamProcessor.getPlayList(); if (downloadFile == null) { //return null; Thread.sleep(100); } if ((new Date().getTime() - now) > config.getFfmpeg().getHls().getTimeout()*1000) { return null; } } while (downloadFile == null); if(file.equals("playlist.m3u8")){ StringBuilder playlistEdit = new StringBuilder(); BufferedReader reader = new BufferedReader(new InputStreamReader(downloadFile)); String currentURL =ProxyLiveUtils.getURL(request); while(reader.ready()) { String line = reader.readLine(); if(line.endsWith(".ts")){ line = currentURL.replace("playlist.m3u8",line); } playlistEdit.append(line).append("\n"); } downloadFile.close(); downloadFile = IOUtils.toInputStream(playlistEdit.toString()); } fileSize = Long.valueOf(downloadFile.available()); } else { downloadFile = hlsStreamProcessor.getSegment(file); if (downloadFile == null) { return null; } fileSize = hlsStreamProcessor.getSegmentSize(file); } respHeaders.setHeader("Connection", "close");//keep-alive respHeaders.setHeader("Access-Control-Allow-Origin","*"); respHeaders.setHeader("Access-Control-Expose-Headers","Content-Length"); respHeaders.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); respHeaders.setHeader(HttpHeaders.CONTENT_LENGTH, fileSize.toString()); respHeaders.setHeader(HttpHeaders.CONTENT_TYPE, getMediaType(file).toString()); return downloadFile; } }