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

Commit

Permalink
Add support for EXT-X-MAP and EXT-X-BYTERANGE (#52)
Browse files Browse the repository at this point in the history
* Add support for EXT-X-MAP

* Supress deprecation warnings in ByteOrderMarkTest

* Add support for EXT-X-BYTERANGE
  • Loading branch information
carlanton authored and sunglee413 committed Oct 20, 2017
1 parent 00a886c commit 0c3d465
Show file tree
Hide file tree
Showing 19 changed files with 540 additions and 69 deletions.
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 @@ -79,13 +80,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 @@ -100,7 +104,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 @@ -20,6 +20,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

0 comments on commit 0c3d465

Please sign in to comment.