Skip to content
This repository has been archived by the owner on Nov 22, 2022. It is now read-only.

Add support for EXT-X-MAP and EXT-X-BYTERANGE #52

Merged
merged 3 commits into from
Oct 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/main/java/com/iheartradio/m3u8/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ final class Constants {
// master playlist tags

public static final String URI = "URI";

public static final String BYTERANGE = "BYTERANGE";

public static final String EXT_X_MEDIA_TAG = "EXT-X-MEDIA";
public static final String TYPE = "TYPE";
public static final String GROUP_ID = "GROUP-ID";
Expand Down Expand Up @@ -78,13 +79,16 @@ final class Constants {
public static final String IV = "IV";
public static final String KEY_FORMAT = "KEYFORMAT";
public static final String KEY_FORMAT_VERSIONS = "KEYFORMATVERSIONS";
public static final String EXT_X_MAP = "EXT-X-MAP";
public static final String EXT_X_BYTERANGE_TAG = "EXT-X-BYTERANGE";

// regular expressions
public static final String YES = "YES";
public static final String NO = "NO";
private static final String INTEGER_REGEX = "\\d+";
private static final String SIGNED_FLOAT_REGEX = "-?\\d*\\.?\\d*";
private static final String TIMESTAMP_REGEX = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?(?:Z?|\\+\\d{2}:\\d{2})?";
private static final String BYTERANGE_REGEX = "(" + INTEGER_REGEX + ")(?:@(" + INTEGER_REGEX + "))?";

public static final Pattern HEXADECIMAL_PATTERN = Pattern.compile("^0[x|X]([0-9A-F]+)$");
public static final Pattern RESOLUTION_PATTERN = Pattern.compile("^(" + INTEGER_REGEX + ")x(" + INTEGER_REGEX + ")$");
Expand All @@ -99,7 +103,9 @@ final class Constants {
public static final Pattern EXT_X_ENDLIST_PATTERN = Pattern.compile("^#" + EXT_X_ENDLIST_TAG + "$");
public static final Pattern EXT_X_I_FRAMES_ONLY_PATTERN = Pattern.compile("^#" + EXT_X_I_FRAMES_ONLY_TAG);
public static final Pattern EXT_X_DISCONTINUITY_PATTERN = Pattern.compile("^#" + EXT_X_DISCONTINUITY_TAG + "$");

public static final Pattern EXT_X_BYTERANGE_PATTERN = Pattern.compile("^#" + EXT_X_BYTERANGE_TAG + EXT_TAG_END + BYTERANGE_REGEX + "$");
public static final Pattern EXT_X_BYTERANGE_VALUE_PATTERN = Pattern.compile("^" + BYTERANGE_REGEX + "$");

// other

public static final int[] UTF_8_BOM_BYTES = {0xEF, 0xBB, 0xBF};
Expand Down
8 changes: 5 additions & 3 deletions src/main/java/com/iheartradio/m3u8/ExtendedM3uParser.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.iheartradio.m3u8;

import com.iheartradio.m3u8.data.Playlist;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import com.iheartradio.m3u8.data.Playlist;

class ExtendedM3uParser extends BaseM3uParser {
private final ParsingMode mParsingMode;
private final Map<String, IExtTagParser> mExtTagParsers = new HashMap<String, IExtTagParser>();
Expand All @@ -32,7 +32,9 @@ class ExtendedM3uParser extends BaseM3uParser {
MasterPlaylistLineParser.EXT_X_I_FRAME_STREAM_INF,
MediaPlaylistLineParser.EXTINF,
MediaPlaylistLineParser.EXT_X_ENDLIST,
MediaPlaylistLineParser.EXT_X_DISCONTINUITY
MediaPlaylistLineParser.EXT_X_DISCONTINUITY,
MediaPlaylistLineParser.EXT_X_MAP,
MediaPlaylistLineParser.EXT_X_BYTERANGE
);
}

Expand Down
11 changes: 4 additions & 7 deletions src/main/java/com/iheartradio/m3u8/MediaParseState.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package com.iheartradio.m3u8;

import com.iheartradio.m3u8.data.*;

import java.util.ArrayList;
import java.util.List;

import com.iheartradio.m3u8.data.EncryptionData;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.PlaylistType;
import com.iheartradio.m3u8.data.StartData;
import com.iheartradio.m3u8.data.TrackData;
import com.iheartradio.m3u8.data.TrackInfo;

class MediaParseState implements PlaylistParseState<MediaPlaylist> {
private List<String> mUnknownTags;
private StartData mStartData;
Expand All @@ -25,6 +20,8 @@ class MediaParseState implements PlaylistParseState<MediaPlaylist> {
public String programDateTime;
public boolean endOfList;
public boolean hasDiscontinuity;
public MapInfo mapInfo;
public ByteRange byteRange;

@Override
public PlaylistParseState<MediaPlaylist> setUnknownTags(final List<String> unknownTags) {
Expand Down
77 changes: 70 additions & 7 deletions src/main/java/com/iheartradio/m3u8/MediaPlaylistLineParser.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package com.iheartradio.m3u8;

import com.iheartradio.m3u8.data.*;
import com.iheartradio.m3u8.data.EncryptionData.Builder;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

import com.iheartradio.m3u8.data.EncryptionData;
import com.iheartradio.m3u8.data.EncryptionData.Builder;
import com.iheartradio.m3u8.data.EncryptionMethod;
import com.iheartradio.m3u8.data.PlaylistType;
import com.iheartradio.m3u8.data.StartData;
import com.iheartradio.m3u8.data.TrackInfo;

class MediaPlaylistLineParser implements LineParser {
private final IExtTagParser tagParser;
private final LineParser lineParser;
Expand Down Expand Up @@ -400,4 +396,71 @@ public void parse(String line, ParseState state) throws ParseException {
state.getMedia().encryptionData = encryptionData;
}
};

static final IExtTagParser EXT_X_MAP = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
private final Map<String, AttributeParser<MapInfo.Builder>> HANDLERS = new HashMap<>();

{
HANDLERS.put(Constants.URI, new AttributeParser<MapInfo.Builder>() {
@Override
public void parse(Attribute attribute, MapInfo.Builder builder, ParseState state) throws ParseException {
builder.withUri(ParseUtil.decodeUri(ParseUtil.parseQuotedString(attribute.value, getTag()), state.encoding));
}
});

HANDLERS.put(Constants.BYTERANGE, new AttributeParser<MapInfo.Builder>() {
@Override
public void parse(Attribute attribute, MapInfo.Builder builder, ParseState state) throws ParseException {
Matcher matcher = Constants.EXT_X_BYTERANGE_VALUE_PATTERN.matcher(ParseUtil.parseQuotedString(attribute.value, getTag()));
if (!matcher.matches()) {
throw ParseException.create(ParseExceptionType.INVALID_BYTERANGE_FORMAT, getTag(), attribute.toString());
}

builder.withByteRange(ParseUtil.matchByteRange(matcher));
}
});
}

@Override
public String getTag() {
return Constants.EXT_X_MAP;
}

@Override
public boolean hasData() {
return true;
}

@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);

final MapInfo.Builder builder = new MapInfo.Builder();

ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
state.getMedia().mapInfo = builder.build();
}
};

static final IExtTagParser EXT_X_BYTERANGE = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);

@Override
public String getTag() {
return Constants.EXT_X_BYTERANGE_TAG;
}

@Override
public boolean hasData() {
return true;
}

@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
final Matcher matcher = ParseUtil.match(Constants.EXT_X_BYTERANGE_PATTERN, line, getTag());
state.getMedia().byteRange = ParseUtil.matchByteRange(matcher);
}
};
}
95 changes: 89 additions & 6 deletions src/main/java/com/iheartradio/m3u8/MediaPlaylistTagWriter.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.iheartradio.m3u8;

import com.iheartradio.m3u8.data.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import com.iheartradio.m3u8.data.EncryptionData;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.StartData;
import com.iheartradio.m3u8.data.TrackData;

abstract class MediaPlaylistTagWriter extends ExtTagWriter {

@Override
Expand Down Expand Up @@ -193,13 +190,20 @@ public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaP
public void write(TagWriter tagWriter, Playlist playlist) throws IOException, ParseException {
if (playlist.hasMediaPlaylist()) {
KeyWriter keyWriter = new KeyWriter();
MapInfoWriter mapInfoWriter = new MapInfoWriter();

for (TrackData trackData : playlist.getMediaPlaylist().getTracks()) {
if (trackData.hasDiscontinuity()) {
tagWriter.writeTag(Constants.EXT_X_DISCONTINUITY_TAG);
}

keyWriter.writeTrackData(tagWriter, playlist, trackData);
mapInfoWriter.writeTrackData(tagWriter, playlist, trackData);

if (trackData.hasByteRange()) {
writeByteRange(tagWriter, trackData.getByteRange());
}

writeExtinf(tagWriter, playlist, trackData);
tagWriter.writeLine(trackData.getUri());
}
Expand All @@ -223,6 +227,19 @@ private static void writeExtinf(TagWriter tagWriter, Playlist playlist, TrackDat
tagWriter.writeTag(Constants.EXTINF_TAG, builder.toString());
}

private static void writeByteRange(TagWriter tagWriter, ByteRange byteRange) throws IOException {
String value;

if (byteRange.getOffset() != null) {
value = String.valueOf(byteRange.getSubRangeLength())
+ '@' + String.valueOf(byteRange.getOffset());
} else {
value = String.valueOf(byteRange.getSubRangeLength());
}

tagWriter.writeTag(Constants.EXT_X_BYTERANGE_TAG, value);
}

static class KeyWriter extends MediaPlaylistTagWriter {
private final Map<String, AttributeWriter<EncryptionData>> HANDLERS = new HashMap<String, AttributeWriter<EncryptionData>>();

Expand Down Expand Up @@ -318,4 +335,70 @@ void writeTrackData(TagWriter tagWriter, Playlist playlist, TrackData trackData)
}
}
}

static class MapInfoWriter extends MediaPlaylistTagWriter {
private final Map<String, AttributeWriter<MapInfo>> HANDLERS = new LinkedHashMap<>();

private MapInfo mMapInfo;

{
HANDLERS.put(Constants.URI, new AttributeWriter<MapInfo>() {
@Override
public String write(MapInfo attributes) throws ParseException {
return WriteUtil.writeQuotedString(attributes.getUri(), getTag());
}

@Override
public boolean containsAttribute(MapInfo attributes) {
return true;
}
});

HANDLERS.put(Constants.BYTERANGE, new AttributeWriter<MapInfo>() {
@Override
public String write(MapInfo attributes) throws ParseException {
ByteRange byteRange = attributes.getByteRange();
String value;
if (byteRange.hasOffset()) {
value = String.valueOf(byteRange.getSubRangeLength())
+ '@' + String.valueOf(byteRange.getOffset());
} else {
value = String.valueOf(byteRange.getSubRangeLength());
}

return WriteUtil.writeQuotedString(value, getTag());
}

@Override
public boolean containsAttribute(MapInfo mapInfo) {
return mapInfo.hasByteRange();
}
});
}

@Override
boolean hasData() {
return true;
}

@Override
public String getTag() {
return Constants.EXT_X_MAP;
}

@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException, ParseException {
writeAttributes(tagWriter, mMapInfo, HANDLERS);
}

void writeTrackData(TagWriter tagWriter, Playlist playlist, TrackData trackData) throws IOException, ParseException {
if (trackData != null && trackData.getMapInfo() != null) {
final MapInfo mapInfo = trackData.getMapInfo();
if (!mapInfo.equals(mMapInfo)) {
mMapInfo = mapInfo;
write(tagWriter, playlist);
}
}
}
}
}
1 change: 1 addition & 0 deletions src/main/java/com/iheartradio/m3u8/ParseExceptionType.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum ParseExceptionType {
INVALID_RESOLUTION_FORMAT("a resolution was not formatted properly"),
INVALID_QUOTED_STRING("a quoted string was not properly formatted"),
INVALID_DATE_TIME_FORMAT("a date-time string was not properly formatted"),
INVALID_BYTERANGE_FORMAT("a byte range string was not properly formatted"),
MASTER_IN_MEDIA("master playlist tags we found in a media playlist"),
MEDIA_IN_MASTER("media playlist tags we found in a master playlist"),
MISSING_ATTRIBUTE_NAME("missing the name of an attribute"),
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/com/iheartradio/m3u8/ParseUtil.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package com.iheartradio.m3u8;

import com.iheartradio.m3u8.data.ByteRange;
import com.iheartradio.m3u8.data.Resolution;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.iheartradio.m3u8.data.Resolution;

final class ParseUtil {
public static int parseInt(String string, String tag) throws ParseException {
try {
Expand Down Expand Up @@ -137,6 +133,12 @@ public static String parseQuotedString(String quotedString, String tag) throws P
return builder.toString();
}

public static ByteRange matchByteRange(Matcher matcher) {
long subRangeLength = Long.parseLong(matcher.group(1));
Long offset = matcher.group(2) != null ? Long.parseLong(matcher.group(2)) : null;
return new ByteRange(subRangeLength, offset);
}

static boolean isWhitespace(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/iheartradio/m3u8/PlaylistError.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,15 @@ public enum PlaylistError {
* The average bandwidth in IFrameStreamInfo must be non-negative or StreamInfo.NO_BANDWIDTH.
*/
I_FRAME_STREAM_WITH_INVALID_AVERAGE_BANDWIDTH,

/**
* MapInfo requires a URI.
*/
MAP_INFO_WITHOUT_URI,

/**
* If a byte range offset is not present, a previous media segment must appear in the playlist
* with a sub-range of the same media resource.
*/
BYTERANGE_WITH_UNDEFINED_OFFSET,
}
Loading