package org.red5.stream.http.servlet; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import javax.ejb.EJB; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.red5.service.httpstream.model.MobileProfile; import com.destinationradiodenver.mobileStreaming.singleton.AvailabilityService; /** * Provides an http stream playlist in m3u8 format. * * Original Concept * @author Paul Gregoire * * Named Pipe and EJB Adaptation * @author Connor Penhale * * HTML status codes used by this servlet: * <pre> * 400 Bad Request * 406 Not Acceptable * 412 Precondition Failed * 417 Expectation Failed * </pre> * * @see * {@link http://tools.ietf.org/html/draft-pantos-http-live-streaming-03} * {@link http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/HTTPStreamingArchitecture/HTTPStreamingArchitecture.html#//apple_ref/doc/uid/TP40008332-CH101-SW2} */ public class PlayList extends HttpServlet { /** * @author Connor Penhale */ private static final long serialVersionUID = 7051372891611466350L; private static final Logger log = Logger.getLogger(PlayList.class.getName()); @EJB private AvailabilityService availabilityService; // number of segments that must exist before displaying any in the playlist private int minimumSegmentCount = 1; @Override public void init(ServletConfig config) throws ServletException { super.init(config); /* * To supply a parameter to a servlet, use a config or init param. * The config-param looks like this: * <pre> * <context-param> * <param-name>startStreamOnRequest</param-name> * <param-value>true</param-value> * </context-param> * </pre> * * And is accessed like so: * config.getServletContext().getInitParameter("startStreamOnRequest") * * An init-param looks like this: * <pre> * <servlet> * <servlet-name>PlayList</servlet-name> * <servlet-class>org.red5.stream.http.servlet.PlayList</servlet-class> * <init-param> * <param-name>startStreamOnRequest</param-name> * <param-value>true</param-value> * </init-param> * </servlet> * </pre> * * And is accessed like so: * getInitParameter("startStreamOnRequest"); * */ String minimumSegmentCountParam = getInitParameter("minimumSegmentCount"); if (!StringUtils.isEmpty(minimumSegmentCountParam)) { minimumSegmentCount = Integer.valueOf(minimumSegmentCountParam); } log.errorf("Minimum segment count - param: %s value: %s", minimumSegmentCountParam, minimumSegmentCount); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { log.errorf("Playlist requested"); String servletPath = request.getServletPath(); /* * Expected Paths: * * /streamName/stream.m3u8 * /streamName/mobileProfileName/stream.m3u8 * */ log.tracef("Posting: %s", servletPath); String streamName = null; String mobileProfileName = null; String[] parts = servletPath.split("/"); log.tracef("Parts: %s", parts.length); if(!(parts.length==3||parts.length==4)){ log.errorf("Servlet Request was not formatted as expected. Expected /streamName/stream.m3u8 or /streamName/mobileProfileName/stream.m3u8 , but got: %s", servletPath); response.sendError(404, "Playlist not found"); return; }else{ if(parts.length>=3){ streamName = parts[1]; log.errorf("Stream Name: %s", streamName); if(parts.length==4){ mobileProfileName = parts[2]; log.errorf("Mobile Profile Name: %s", mobileProfileName); } } } //write the playlist PrintWriter writer = response.getWriter(); int segmentDuration = availabilityService.getSegmentTimeLimit() / 1000; // The amount of time a Origin-Pull CDN should cache an m3u8 playlist is 1 second shorter than the duration of a segment int cdnMaxAgeSeconds = segmentDuration - 1; response.setHeader("Cache-Control", "max-age="+cdnMaxAgeSeconds); // TODO: define if needs to be tunable response.setContentType("application/x-mpegURL"); //Determine if mobile profile or adaptive bitrate playlist if(mobileProfileName!=null){ /* * Stream Playlist (Not root, adaptive playlist) * * Example: * * #EXTM3U * #EXT-X-ALLOW-CACHE:NO * #EXT-X-MEDIA-SEQUENCE:0 * #EXT-X-TARGETDURATION:10 * * #EXTINF:10, * http://media.example.com/segment1.ts * #EXTINF:10, * http://media.example.com/segment2.ts * #EXTINF:10, * http://media.example.com/segment3.ts * #EXT-X-ENDLIST */ writer.println("#EXTM3U\n#EXT-X-ALLOW-CACHE:NO\n"); if(availabilityService.isAvailable(streamName, mobileProfileName)){ log.errorf("PLS for Stream: %s is available", streamName); int count = availabilityService.getSegmentCount(streamName, mobileProfileName); if (count < minimumSegmentCount) { log.errorf("Starting wait loop for segment availability"); long maxWaitTime = minimumSegmentCount * availabilityService.getSegmentTimeLimit(); long start = System.currentTimeMillis(); do { try { Thread.sleep(500); } catch (InterruptedException e) { // } if ((System.currentTimeMillis() - start) >= maxWaitTime) { log.infof("Maximum segment wait time exceeded for %s/%s", streamName, mobileProfileName); break; } } while ((count = availabilityService.getSegmentCount(streamName , mobileProfileName)) < minimumSegmentCount); } // get the count one last time count = availabilityService.getSegmentCount(streamName , mobileProfileName); log.errorf("Segment count: %s", count); if (count >= minimumSegmentCount) { //get segment duration in seconds //get current sequence number int sequenceNumber = availabilityService.getSegmentIndex(streamName, mobileProfileName); log.tracef("Current sequence number: %s", sequenceNumber); /* HTTP streaming spec section 3.2.2 Each media file URI in a Playlist has a unique sequence number. The sequence number of a URI is equal to the sequence number of the URI that preceded it plus one. The EXT-X-MEDIA-SEQUENCE tag indicates the sequence number of the first URI that appears in a Playlist file. */ // determine the lowest sequence number int lowestSequenceNumber = Math.max(-1, sequenceNumber - availabilityService.getNumMaxSegments()) + 1; log.tracef("Lowest sequence number: %s", lowestSequenceNumber); // for logging StringBuilder sb = new StringBuilder(); // create the heading String playListHeading = String.format("#EXT-X-TARGETDURATION:%s\n#EXT-X-MEDIA-SEQUENCE:%s\n", segmentDuration, lowestSequenceNumber); writer.println(playListHeading); sb.append(playListHeading); //loop through the x closest segments for (int s = lowestSequenceNumber; s <= sequenceNumber; s++) { String playListEntry = String.format( "#EXTINF:%s, no desc\nstream%s.ts\n", segmentDuration, s); writer.println(playListEntry); sb.append(playListEntry); } // are we on the last segment? // Live streams are never on their last segment /*if (segment.isLast()) { log.errorf("Last segment"); writer.println("#EXT-X-ENDLIST\n"); sb.append("#EXT-X-ENDLIST\n"); } */ log.errorf(sb.toString()); } else { log .tracef( "Minimum segment count not yet reached, currently at: %s", count); } } } else { /* * Root / Adaptive Playlist * * Example: * * #EXTM3U * #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=96000 * StreamName/2G/stream.m3u8 * #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=256000 * StreamName/4G/stream.m3u8 * #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000 * StreamName/WiFi/stream.m3u8 * */ writer.println("#EXTM3U\n"); if (availabilityService.isADPAvailable(streamName)) { log.errorf("ADP PLS for Stream: %s is available", streamName); // remove it from requested list if its there ArrayList<MobileProfile> mobileProfiles = availabilityService.getMobileProfiles(streamName); if(mobileProfiles!=null) log.tracef("There are %s mobileProfiles in the profileMap for %s", mobileProfiles.size(), streamName); for (MobileProfile mp : mobileProfiles) { String pLHeade = String.format("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=%s", mp.getBandwidth()); writer.println(pLHeade); String pLEntry = String.format("%s/stream.m3u8", mp.getName()); writer.println(pLEntry); } } } writer.flush(); } }