package com.zhan_dui.utils.m3u8;
import java.io.File;
import java.net.URI;
import java.nio.channels.Channels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.zhan_dui.utils.m3u8.M3uConstants.*;
/**
* Implementation based on http://tools.ietf.org/html/draft-pantos-http-live-streaming-02#section-3.1.
*
* @author dkuffner
*/
final class PlaylistParser {
private Logger log = Logger.getLogger(getClass().getName());
static PlaylistParser create(PlaylistType type) {
return new PlaylistParser(type);
}
private PlaylistType type;
public PlaylistParser(PlaylistType type) {
if (type == null) {
throw new NullPointerException("type"); //NonNls
}
this.type = type;
}
/**
* See {@link Channels#newReader(java.nio.channels.ReadableByteChannel, String)}
* See {@link java.io.StringReader}
*
* @param source the source.
* @return a playlist.
* @throws ParseException parsing fails.
*/
public Playlist parse(Readable source) throws ParseException {
final Scanner scanner = new Scanner(source);
boolean firstLine = true;
int lineNumber = 0;
final List<Element> elements = new ArrayList<Element>(10);
final ElementBuilder builder = new ElementBuilder();
boolean endListSet = false;
int targetDuration = -1;
int mediaSequenceNumber = -1;
EncryptionInfo currentEncryption = null;
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
if (line.length() > 0) {
if (line.startsWith(EX_PREFIX)) {
if (firstLine) {
checkFirstLine(lineNumber, line);
firstLine = false;
} else if (line.startsWith(EXTINF)) {
parseExtInf(line, lineNumber, builder);
} else if (line.startsWith(EXT_X_ENDLIST)) {
endListSet = true;
} else if (line.startsWith(EXT_X_TARGET_DURATION)) {
if (targetDuration != -1) {
throw new ParseException(line, lineNumber, EXT_X_TARGET_DURATION + " duplicated");
}
targetDuration = parseTargetDuration(line, lineNumber);
} else if (line.startsWith(EXT_X_MEDIA_SEQUENCE)) {
if (mediaSequenceNumber != -1) {
throw new ParseException(line, lineNumber, EXT_X_MEDIA_SEQUENCE + " duplicated");
}
mediaSequenceNumber = parseMediaSequence(line, lineNumber);
} else if (line.startsWith(EXT_X_DISCONTINUITY)) {
builder.discontinuity(true);
} else if (line.startsWith(EXT_X_PROGRAM_DATE_TIME)) {
long programDateTime = parseProgramDateTime(line, lineNumber);
builder.programDate(programDateTime);
} else if (line.startsWith(EXT_X_KEY)) {
currentEncryption = parseEncryption(line, lineNumber);
} else {
log.log(Level.FINE, new StringBuilder().append("Unknown: '").append(line).append("'").toString());
}
} else if (line.startsWith(COMMENT_PREFIX)) {
// no first line check because comments will be ignored.
// comment do nothing
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "----- Comment: " + line);
}
} else {
if (firstLine) {
checkFirstLine(lineNumber, line);
}
// No prefix: must be the media uri.
builder.encrypted(currentEncryption);
builder.uri(toURI(line));
elements.add(builder.create());
// a new element begins.
builder.reset();
}
}
lineNumber++;
}
return new Playlist(Collections.unmodifiableList(elements), endListSet, targetDuration, mediaSequenceNumber);
}
private URI toURI(String line) {
try {
return (URI.create(line));
} catch (IllegalArgumentException e) {
return new File(line).toURI();
}
}
private long parseProgramDateTime(String line, int lineNumber) throws ParseException {
return Patterns.toDate(line, lineNumber);
}
private int parseTargetDuration(String line, int lineNumber) throws ParseException {
return (int) parseNumberTag(line, lineNumber, Patterns.EXT_X_TARGET_DURATION, EXT_X_TARGET_DURATION);
}
private int parseMediaSequence(String line, int lineNumber) throws ParseException {
return (int) parseNumberTag(line, lineNumber, Patterns.EXT_X_MEDIA_SEQUENCE, EXT_X_MEDIA_SEQUENCE);
}
private long parseNumberTag(String line, int lineNumber, Pattern patter, String property) throws ParseException {
Matcher matcher = patter.matcher(line);
if (!matcher.find() && !matcher.matches() && matcher.groupCount() < 1) {
throw new ParseException(line, lineNumber, property + " must specify duration");
}
try {
return Long.valueOf(matcher.group(1));
} catch (NumberFormatException e) {
// should not happen because of
throw new ParseException(line, lineNumber, e);
}
}
private void checkFirstLine(int lineNumber, String line) throws ParseException {
if (type == PlaylistType.M3U8 && !line.startsWith(EXTM3U)) {
throw new ParseException(line, lineNumber, "Playlist type '" + PlaylistType.M3U8 + "' must start with " + EXTM3U);
}
}
private void parseExtInf(String line, int lineNumber, ElementBuilder builder) throws ParseException {
// EXTINF:200,Title
final Matcher matcher = Patterns.EXTINF.matcher(line);
if (!matcher.find() && !matcher.matches() && matcher.groupCount() < 1) {
throw new ParseException(line, lineNumber, "EXTINF must specify at least the duration");
}
String duration = matcher.group(1);
String title = matcher.groupCount() > 1 ? matcher.group(2) : "";
try {
builder.duration(Double.valueOf(duration)).title(title);
} catch (NumberFormatException e) {
// should not happen because of
throw new ParseException(line, lineNumber, e);
}
}
private EncryptionInfo parseEncryption(String line, int lineNumber) throws ParseException {
Matcher matcher = Patterns.EXT_X_KEY.matcher(line);
if (!matcher.find() || !matcher.matches() || matcher.groupCount() < 1) {
throw new ParseException(line, lineNumber, "illegal input: " + line);
}
String method = matcher.group(1);
String uri = matcher.group(3);
if (method.equalsIgnoreCase("none")) {
return null;
}
return new ElementImpl.EncryptionInfoImpl(uri != null ? toURI(uri) : null, method);
}
}