This commit is contained in:
J62 2025-03-12 19:15:12 -07:00
commit b34f82bf6a
96 changed files with 7586 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs
hs_err_pid*
# android
local.properties
# gradle
/.gradle/
build/
# idea
/.idea/
*.iml
/bin/
/.settings/
/.classpath
/.project
target

45
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,45 @@
## How can I contribute?
* create issues for bugs you find
* create pull requests to fix bigs, add features, clean up code, etc.
* improve the documentation
* chat with us on the freenode IRC server at #open-m3u8
## What can I work on?
We do not yet support the full m3u8 specification! So a great way to help is by adding support for more tags in the specification:
http://tools.ietf.org/html/draft-pantos-http-live-streaming-14
The issues page is another good place to look for ways to contribute.
## Compatibility
This library needs to support Android which means we are limited to Java 7 sans try-with-resources.
## Code Style
* 4 spaces per indent - no tab characters
* mPrefix private members
* opening braces on the same line
* when wrapping long method calls, put each argument on its own line
* when building long fluent builders, put each method in the chain on its own line
## Merging
We use a rebase / cherry-pick strategy to merging code. This is to maintain a legible git history on the master branch. This has a few implications:
* it is best if you rebase your branches onto master to fix merge conflicts instead of merging
* your commits may be squashed, reordered, reworded, or edited when merged
* your pull request will be marked closed instead of merged but will be linked to the closing commit
* your branch will not remain tracked by this repository
## Working with the Code
### com.iheartradio.m3u8 package
Everything not meant to be visible to the public API must be package protected. If a whole class is package protected, then you may mark the fields public since they will still not be visible.
### com.iheartradio.m3u8.data package
The data structures in this package reflect the structure of a playlist based on the specification. They are part of the public API and must be immutable. The `Playlist` is the result of parsing and the input of writing.

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2014 iHeartMedia Inc.
Source: https://github.com/iheartradio/open-m3u8
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.

173
README.md Normal file
View File

@ -0,0 +1,173 @@
# Open M3U8 (working name)
## Description
This is an open source M3U8 playlist parser and writer java library that attempts to conform to this specification:
http://tools.ietf.org/html/draft-pantos-http-live-streaming-16
Currently the functionality is more than sufficient for our needs. However, there is still a lot of work to be done before we have full compliance. Pull requests are welcome!
## Rationale
We would like to give back to the open source community surrounding Android that has helped make iHeartRadio a success. By using the MIT license we hope to make this code as usable as possible.
## Artifacts
We now have artifacts in Maven Central! Artifacts are typically built with Java 7.
### Gradle
```
dependencies {
compile 'com.iheartradio.m3u8:open-m3u8:0.2.4'
}
```
### Maven
```
<dependency>
<groupId>com.iheartradio.m3u8</groupId>
<artifactId>open-m3u8</artifactId>
<version>0.2.4</version>
</dependency>
```
## Getting started
Important: The public API is still volatile. It will remain subject to frequent change until a 1.0.0 release is made.
Getting started with parsing is quite easy: Get a `PlaylistParser` and specify the format.
```java
InputStream inputStream = ...
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
Playlist playlist = parser.parse();
```
Creating a new `Playlist` works via `Builder`s and their fluent `with*()` methods. On each `build()` method the provided parameters are validated:
```java
TrackData trackData = new TrackData.Builder()
.withTrackInfo(new TrackInfo(3.0f, "Example Song"))
.withPath("example.mp3")
.build();
List<TrackData> tracks = new ArrayList<TrackData>();
tracks.add(trackData);
MediaPlaylist mediaPlaylist = new MediaPlaylist.Builder()
.withMediaSequenceNumber(1)
.withTargetDuration(3)
.withTracks(tracks)
.build();
Playlist playlist = new Playlist.Builder()
.withCompatibilityVersion(5)
.withMediaPlaylist(mediaPlaylist)
.build();
```
The Playlist is similar to a C style union of a `MasterPlaylist` and `MediaPlaylist` in that it has one or the other but not both. You can check with `Playlist.hasMasterPlaylist()` or `Playlist.hasMediaPlaylist()` which type you got.
Modifying an existing `Playlist` works similar to creating via the `Builder`s. Also, each data class provides a `buildUpon()` method to generate a new `Builder` with all the data from the object itself:
```java
TrackData additionalTrack = new TrackData.Builder()
.withTrackInfo(new TrackInfo(3.0f, "Additional Song"))
.withPath("additional.mp3")
.build();
List<TrackData> updatedTracks = new ArrayList<TrackData>(playlist.getMediaPlaylist().getTracks());
updatedTracks.add(additionalTrack);
MediaPlaylist updatedMediaPlaylist = playlist.getMediaPlaylist()
.buildUpon()
.withTracks(updatedTracks)
.build();
Playlist updatedPlaylist = playlist.buildUpon()
.withMediaPlaylist(updatedMediaPlaylist)
.build();
```
A `PlaylistWriter` can be obtained directly or via its builder.
```java
OutputStream outputStream = ...
PlaylistWriter writer = new PlaylistWriter(outputStream, Format.EXT_M3U, Encoding.UTF_8);
writer.write(updatedPlaylist);
writer = new PlaylistWriter.Builder()
.withOutputStream(outputStream)
.withFormat(Format.EXT_M3U)
.withEncoding(Encoding.UTF_8)
.build();
writer.write(updatedPlaylist);
```
causing this playlist to be written:
```
#EXTM3U
#EXT-X-VERSION:5
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:3.0,Example Song
example.mp3
#EXTINF:3.0,Additional Song
additional.mp3
#EXT-X-ENDLIST
```
Currently, writing multiple playlists with the same writer is not supported.
## Advanced usage
### Parsing mode
The parser supports a mode configuration - by default it operats in a `strict` mode which attemps to adhere to the specification as much as possible.
Providing the parser a `ParsingMode` you can relax some of the requirements. Two parsing modes are made available, or you can build your own custom mode.
```java
ParsingMode.LENIENT // lenient about everything
ParsingMode.STRICT // strict about everything
```
Example:
```java
InputStream inputStream = ...
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
if (playlist.hasMasterPlaylist() && playlist.getMasterPlaylist().hasUnknownTags()) {
System.err.println(
playlist.getMasterPlaylist().getUnknownTags());
} else if (playlist.hasMediaPlaylist() && playlist.getMediaPlaylist().hasUnknownTags()) {
System.err.println(
playlist.getMediaPlaylist().getUnknownTags());
} else {
System.out.println("Parsing without unknown tags successful");
}
```
=======
## Build
This is a Gradle project. Known compatible gradle versions:
- 2.1
- 2.4
Build and test via:
```
gradle build
```
The output can be found in the generated /build/libs/ dir.
Cobertura is configured to report the line coverage:
```
gradle cobertura
```
producing the coverage report at `build/reports/cobertura/index.html`

25
pom.xml Normal file
View File

@ -0,0 +1,25 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.iheartradio.m3u8</groupId>
<artifactId>open-m3u8</artifactId>
<version>0.2.7-CTBREC</version>
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,20 @@
package com.iheartradio.m3u8;
class Attribute {
public final String name;
public final String value;
Attribute(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String toString() {
return new StringBuilder()
.append("(Attribute")
.append(" name=").append(name)
.append(" value=").append(value)
.append(")").toString();
}
}

View File

@ -0,0 +1,6 @@
package com.iheartradio.m3u8;
interface AttributeParser<Builder> {
void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException;
}

View File

@ -0,0 +1,8 @@
package com.iheartradio.m3u8;
interface AttributeWriter<T> {
String write(T attributes) throws ParseException;
boolean containsAttribute(T attributes);
}

View File

@ -0,0 +1,25 @@
package com.iheartradio.m3u8;
import java.io.EOFException;
import java.io.InputStream;
abstract class BaseM3uParser implements IPlaylistParser {
protected final M3uScanner mScanner;
protected final Encoding mEncoding;
BaseM3uParser(InputStream inputStream, Encoding encoding) {
mScanner = new M3uScanner(inputStream, encoding);
mEncoding = encoding;
}
@Override
public boolean isAvailable() {
return mScanner.hasNext();
}
final void validateAvailable() throws EOFException {
if (!isAvailable()) {
throw new EOFException();
}
}
}

View File

@ -0,0 +1,121 @@
package com.iheartradio.m3u8;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
final class Constants {
public static final String MIME_TYPE = "application/vnd.apple.mpegurl";
public static final String MIME_TYPE_COMPATIBILITY = "audio/mpegurl";
public static final String ATTRIBUTE_SEPARATOR = "=";
public static final char COMMA_CHAR = ',';
public static final String COMMA = Character.toString(COMMA_CHAR);
public static final String ATTRIBUTE_LIST_SEPARATOR = COMMA;
public static final String LIST_SEPARATOR = "/";
public static final String COMMENT_PREFIX = "#";
public static final String EXT_TAG_PREFIX = "#EXT";
public static final String EXT_TAG_END = ":";
public static final String WRITE_NEW_LINE = "\n";
public static final String PARSE_NEW_LINE = "\\r?\\n";
// extension tags
public static final String EXTM3U_TAG = "EXTM3U";
public static final String EXT_X_VERSION_TAG = "EXT-X-VERSION";
// 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";
public static final String LANGUAGE = "LANGUAGE";
public static final String ASSOCIATED_LANGUAGE = "ASSOC-LANGUAGE";
public static final String NAME = "NAME";
public static final String DEFAULT = "DEFAULT";
public static final String AUTO_SELECT = "AUTOSELECT";
public static final String FORCED = "FORCED";
public static final String IN_STREAM_ID = "INSTREAM-ID";
public static final String CHARACTERISTICS = "CHARACTERISTICS";
public static final String CHANNELS = "CHANNELS";
public static final String EXT_X_STREAM_INF_TAG = "EXT-X-STREAM-INF";
public static final String EXT_X_I_FRAME_STREAM_INF_TAG = "EXT-X-I-FRAME-STREAM-INF";
public static final String BANDWIDTH = "BANDWIDTH";
public static final String AVERAGE_BANDWIDTH = "AVERAGE-BANDWIDTH";
public static final String CODECS = "CODECS";
public static final String RESOLUTION = "RESOLUTION";
public static final String FRAME_RATE = "FRAME-RATE";
public static final String VIDEO = "VIDEO";
public static final String PROGRAM_ID = "PROGRAM-ID";
public static final String AUDIO = "AUDIO";
public static final String SUBTITLES = "SUBTITLES";
public static final String CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
// media playlist tags
public static final String EXT_X_PLAYLIST_TYPE_TAG = "EXT-X-PLAYLIST-TYPE";
public static final String EXT_X_PROGRAM_DATE_TIME_TAG = "EXT-X-PROGRAM-DATE-TIME";
public static final String EXT_X_TARGETDURATION_TAG = "EXT-X-TARGETDURATION";
public static final String EXT_X_START_TAG = "EXT-X-START";
public static final String TIME_OFFSET = "TIME-OFFSET";
public static final String PRECISE = "PRECISE";
public static final String EXT_X_MEDIA_SEQUENCE_TAG = "EXT-X-MEDIA-SEQUENCE";
public static final String EXT_X_ALLOW_CACHE_TAG = "EXT-X-ALLOW-CACHE";
public static final String EXT_X_ENDLIST_TAG = "EXT-X-ENDLIST";
public static final String EXT_X_I_FRAMES_ONLY_TAG = "EXT-X-I-FRAMES-ONLY";
public static final String EXT_X_DISCONTINUITY_TAG = "EXT-X-DISCONTINUITY";
// media segment tags
public static final String EXTINF_TAG = "EXTINF";
public static final String EXT_X_KEY_TAG = "EXT-X-KEY";
public static final String METHOD = "METHOD";
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 + ")$");
public static final Pattern EXT_X_VERSION_PATTERN = Pattern.compile("^#" + EXT_X_VERSION_TAG + EXT_TAG_END + "(" + INTEGER_REGEX + ")$");
public static final Pattern EXT_X_TARGETDURATION_PATTERN = Pattern.compile("^#" + EXT_X_TARGETDURATION_TAG + EXT_TAG_END + "(" + INTEGER_REGEX + ")$");
public static final Pattern EXT_X_MEDIA_SEQUENCE_PATTERN = Pattern.compile("^#" + EXT_X_MEDIA_SEQUENCE_TAG + EXT_TAG_END + "(" + INTEGER_REGEX + ")$");
public static final Pattern EXT_X_PLAYLIST_TYPE_PATTERN = Pattern.compile("^#" + EXT_X_PLAYLIST_TYPE_TAG + EXT_TAG_END + "(EVENT|VOD)$");
public static final Pattern EXT_X_PROGRAM_DATE_TIME_PATTERN = Pattern.compile("^#" + EXT_X_PROGRAM_DATE_TIME_TAG + EXT_TAG_END + "(" + TIMESTAMP_REGEX + ")$");
public static final Pattern EXT_X_MEDIA_IN_STREAM_ID_PATTERN = Pattern.compile("^CC[1-4]|SERVICE(?:[1-9]|[1-5]\\d|6[0-3])$");
public static final Pattern EXTINF_PATTERN = Pattern.compile("^#" + EXTINF_TAG + EXT_TAG_END + "(" + SIGNED_FLOAT_REGEX + ")(?:,(.+)?)?$");
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};
public static final char UNICODE_BOM = '\uFEFF';
public static final int MAX_COMPATIBILITY_VERSION = Integer.MAX_VALUE;
public static final int IV_SIZE = 16;
//Against the spec but used by Adobe
public static final int IV_SIZE_ALTERNATIVE = 32;
public static final String DEFAULT_KEY_FORMAT = "identity";
public static final String NO_CLOSED_CAPTIONS = "NONE";
public static final List<Integer> DEFAULT_KEY_FORMAT_VERSIONS = Arrays.asList(1);
}

View File

@ -0,0 +1,41 @@
package com.iheartradio.m3u8;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public enum Encoding {
UTF_8("utf-8", true),
WINDOWS_1252("windows-1252", false);
private static final Map<String, Encoding> sMap = new HashMap<String, Encoding>();
final String value;
final boolean supportsByteOrderMark;
static {
for (Encoding mediaType : Encoding.values()) {
sMap.put(mediaType.value, mediaType);
}
}
private Encoding(String value, boolean supportsByteOrderMark) {
this.value = value;
this.supportsByteOrderMark = supportsByteOrderMark;
}
/**
* @return the encoding for the given value if supported, if the encoding is unsupported or null, null will be returned
*/
public static Encoding fromValue(String value) {
if (value == null) {
return null;
} else {
return sMap.get(value.toLowerCase(Locale.US));
}
}
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,144 @@
package com.iheartradio.m3u8;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.StartData;
class ExtLineParser implements LineParser {
private final IExtTagParser mTagParser;
ExtLineParser(IExtTagParser tagParser) {
mTagParser = tagParser;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
if (mTagParser.hasData()) {
if (line.indexOf(Constants.EXT_TAG_END) != mTagParser.getTag().length() + 1) {
throw ParseException.create(ParseExceptionType.MISSING_EXT_TAG_SEPARATOR, mTagParser.getTag(), line);
}
}
}
static final IExtTagParser EXTM3U_HANDLER = new IExtTagParser() {
@Override
public String getTag() {
return Constants.EXTM3U_TAG;
}
@Override
public boolean hasData() {
return false;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
if (state.isExtended()) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
state.setExtended();
}
};
static final IExtTagParser EXT_UNKNOWN_HANDLER = new IExtTagParser() {
@Override
public String getTag() {
return null;
}
@Override
public boolean hasData() {
return false;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
state.unknownTags.add(line);
}
};
static final IExtTagParser EXT_X_VERSION_HANDLER = new IExtTagParser() {
private final ExtLineParser lineParser = new ExtLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_VERSION_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_VERSION_PATTERN, line, getTag());
if (state.getCompatibilityVersion() != ParseState.NONE) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
final int compatibilityVersion = ParseUtil.parseInt(matcher.group(1), getTag());
if (compatibilityVersion < Playlist.MIN_COMPATIBILITY_VERSION) {
throw ParseException.create(ParseExceptionType.INVALID_COMPATIBILITY_VERSION, getTag(), line);
}
if (compatibilityVersion > Constants.MAX_COMPATIBILITY_VERSION) {
throw ParseException.create(ParseExceptionType.UNSUPPORTED_COMPATIBILITY_VERSION, getTag(), line);
}
state.setCompatibilityVersion(compatibilityVersion);
}
};
static final IExtTagParser EXT_X_START = new IExtTagParser() {
private final LineParser lineParser = new ExtLineParser(this);
private final Map<String, AttributeParser<StartData.Builder>> HANDLERS = new HashMap<>();
{
HANDLERS.put(Constants.TIME_OFFSET, new AttributeParser<StartData.Builder>() {
@Override
public void parse(Attribute attribute, StartData.Builder builder, ParseState state) throws ParseException {
builder.withTimeOffset(ParseUtil.parseFloat(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.PRECISE, new AttributeParser<StartData.Builder>() {
@Override
public void parse(Attribute attribute, StartData.Builder builder, ParseState state) throws ParseException {
builder.withPrecise(ParseUtil.parseYesNo(attribute, getTag()));
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_START_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
if (state.startData != null) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
final StartData.Builder builder = new StartData.Builder();
lineParser.parse(line, state);
ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
state.startData = builder.build();
}
};
}

View File

@ -0,0 +1,96 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.iheartradio.m3u8.data.Playlist;
abstract class ExtTagWriter implements IExtTagWriter {
@Override
public void write(TagWriter tagWriter, Playlist playlist) throws IOException, ParseException {
if (!hasData()) {
tagWriter.writeTag(getTag());
}
}
abstract boolean hasData();
<T> void writeAttributes(TagWriter tagWriter, T attributes, Map<String, ? extends AttributeWriter<T>> attributeWriters) throws IOException, ParseException {
StringBuilder sb = new StringBuilder();
for(Map.Entry<String, ? extends AttributeWriter<T>> entry : attributeWriters.entrySet()) {
AttributeWriter<T> handler = entry.getValue();
String attribute = entry.getKey();
if (handler.containsAttribute(attributes)) {
String value = handler.write(attributes);
sb.append(attribute).append(Constants.ATTRIBUTE_SEPARATOR).append(value);
sb.append(Constants.ATTRIBUTE_LIST_SEPARATOR);
}
}
sb.deleteCharAt(sb.length() - 1);
tagWriter.writeTag(getTag(), sb.toString());
}
static final IExtTagWriter EXTM3U_HANDLER = new ExtTagWriter() {
@Override
public String getTag() {
return Constants.EXTM3U_TAG;
}
@Override
boolean hasData() {
return false;
}
};
static final IExtTagWriter EXT_UNKNOWN_HANDLER = new ExtTagWriter() {
@Override
public String getTag() {
return null;
}
@Override
boolean hasData() {
return true;
}
@Override
public void write(TagWriter tagWriter, Playlist playlist) throws IOException {
List<String> unknownTags;
if (playlist.hasMasterPlaylist() && playlist.getMasterPlaylist().hasUnknownTags()) {
unknownTags = playlist.getMasterPlaylist().getUnknownTags();
} else if (playlist.getMediaPlaylist().hasUnknownTags()) {
unknownTags = playlist.getMediaPlaylist().getUnknownTags();
} else {
unknownTags = Collections.emptyList();
}
for(String line : unknownTags) {
tagWriter.writeLine(line);
}
}
};
static final IExtTagWriter EXT_X_VERSION_HANDLER = new ExtTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_VERSION_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void write(TagWriter tagWriter, Playlist playlist) throws IOException {
tagWriter.writeTag(getTag(), Integer.toString(playlist.getCompatibilityVersion()));
}
};
}

View File

@ -0,0 +1,134 @@
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;
class ExtendedM3uParser extends BaseM3uParser {
private final ParsingMode mParsingMode;
private final Map<String, IExtTagParser> mExtTagParsers = new HashMap<String, IExtTagParser>();
ExtendedM3uParser(InputStream inputStream, Encoding encoding, ParsingMode parsingMode) {
super(inputStream, encoding);
mParsingMode = parsingMode;
// TODO implement the remaining EXT tag handlers and add them here
putParsers(
ExtLineParser.EXTM3U_HANDLER,
ExtLineParser.EXT_X_VERSION_HANDLER,
ExtLineParser.EXT_X_START,
MediaPlaylistLineParser.EXT_X_PLAYLIST_TYPE,
MediaPlaylistLineParser.EXT_X_PROGRAM_DATE_TIME,
MediaPlaylistLineParser.EXT_X_KEY,
MediaPlaylistLineParser.EXT_X_TARGETDURATION,
MediaPlaylistLineParser.EXT_X_MEDIA_SEQUENCE,
MediaPlaylistLineParser.EXT_X_I_FRAMES_ONLY,
MasterPlaylistLineParser.EXT_X_MEDIA,
MediaPlaylistLineParser.EXT_X_ALLOW_CACHE,
MasterPlaylistLineParser.EXT_X_STREAM_INF,
MasterPlaylistLineParser.EXT_X_I_FRAME_STREAM_INF,
MediaPlaylistLineParser.EXTINF,
MediaPlaylistLineParser.EXT_X_ENDLIST,
MediaPlaylistLineParser.EXT_X_DISCONTINUITY,
MediaPlaylistLineParser.EXT_X_MAP,
MediaPlaylistLineParser.EXT_X_BYTERANGE
);
}
@Override
public Playlist parse() throws IOException, ParseException, PlaylistException {
validateAvailable();
final ParseState state = new ParseState(mEncoding);
final LineParser playlistParser = new PlaylistLineParser();
final LineParser trackLineParser = new TrackLineParser();
try {
while (mScanner.hasNext()) {
final String line = mScanner.next();
checkWhitespace(line);
if (line.length() == 0 || isComment(line)) {
continue;
} else {
if (isExtTag(line)) {
final String tagKey = getExtTagKey(line);
IExtTagParser tagParser = mExtTagParsers.get(tagKey);
if (tagParser == null) {
//To support forward compatibility, when parsing Playlists, Clients
//MUST:
//o ignore any unrecognized tags.
if (mParsingMode.allowUnknownTags) {
tagParser = ExtLineParser.EXT_UNKNOWN_HANDLER;
} else {
throw ParseException.create(ParseExceptionType.UNSUPPORTED_EXT_TAG_DETECTED, tagKey, line);
}
}
tagParser.parse(line, state);
if (state.isMedia() && state.getMedia().endOfList) {
break;
}
} else if (state.isMaster()) {
playlistParser.parse(line, state);
} else if (state.isMedia()) {
trackLineParser.parse(line, state);
} else {
throw ParseException.create(ParseExceptionType.UNKNOWN_PLAYLIST_TYPE, line);
}
}
}
final Playlist playlist = state.buildPlaylist();
final PlaylistValidation validation = PlaylistValidation.from(playlist, mParsingMode);
if (validation.isValid()) {
return playlist;
} else {
throw new PlaylistException(mScanner.getInput(), validation.getErrors());
}
} catch (ParseException exception) {
exception.setInput(mScanner.getInput());
throw exception;
}
}
private void putParsers(IExtTagParser... parsers) {
if (parsers != null) {
for (IExtTagParser parser : parsers) {
mExtTagParsers.put(parser.getTag(), parser);
}
}
}
private void checkWhitespace(final String line) throws ParseException {
if (!isComment(line)) {
if (line.length() != line.trim().length()) {
throw ParseException.create(ParseExceptionType.WHITESPACE_IN_TRACK, line);
}
}
}
private boolean isComment(final String line) {
return line.startsWith(Constants.COMMENT_PREFIX) && !isExtTag(line);
}
private boolean isExtTag(final String line) {
return line.startsWith(Constants.EXT_TAG_PREFIX);
}
private String getExtTagKey(final String line) {
int index = line.indexOf(Constants.EXT_TAG_END);
if (index == -1) {
return line.substring(1);
} else {
return line.substring(1, index);
}
}
}

View File

@ -0,0 +1,46 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.iheartradio.m3u8.data.Playlist;
class ExtendedM3uWriter extends Writer {
private List<SectionWriter> mExtTagWriter = new ArrayList<SectionWriter>();
public ExtendedM3uWriter(OutputStream outputStream, Encoding encoding) {
super(outputStream, encoding);
// Order influences output in file!
putWriters(
ExtTagWriter.EXTM3U_HANDLER,
ExtTagWriter.EXT_X_VERSION_HANDLER,
MediaPlaylistTagWriter.EXT_X_PLAYLIST_TYPE,
MediaPlaylistTagWriter.EXT_X_TARGETDURATION,
MediaPlaylistTagWriter.EXT_X_START,
MediaPlaylistTagWriter.EXT_X_MEDIA_SEQUENCE,
MediaPlaylistTagWriter.EXT_X_I_FRAMES_ONLY,
MasterPlaylistTagWriter.EXT_X_MEDIA,
MediaPlaylistTagWriter.EXT_X_ALLOW_CACHE,
MasterPlaylistTagWriter.EXT_X_STREAM_INF,
MasterPlaylistTagWriter.EXT_X_I_FRAME_STREAM_INF,
MediaPlaylistTagWriter.MEDIA_SEGMENTS,
MediaPlaylistTagWriter.EXT_X_ENDLIST
);
}
private void putWriters(SectionWriter... writers) {
if (writers != null) {
Collections.addAll(mExtTagWriter, writers);
}
}
@Override
void doWrite(Playlist playlist) throws IOException, ParseException, PlaylistException {
for (SectionWriter singleTagWriter : mExtTagWriter) {
singleTagWriter.write(tagWriter, playlist);
}
}
}

View File

@ -0,0 +1,45 @@
package com.iheartradio.m3u8;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public enum Extension {
M3U("m3u", Encoding.WINDOWS_1252),
M3U8("m3u8", Encoding.UTF_8);
private static final Map<String, Extension> sMap = new HashMap<String, Extension>();
static {
for (Extension mediaType : Extension.values()) {
sMap.put(mediaType.value, mediaType);
}
}
final String value;
final Encoding encoding;
private Extension(String value, Encoding encoding) {
this.value = value;
this.encoding = encoding;
}
/**
* @return the extension for the given value if supported, if the extension is unsupported or null, null will be returned
*/
public static Extension fromValue(String value) {
if (value == null) {
return null;
} else {
return sMap.get(value.toLowerCase(Locale.US));
}
}
public String getValue() {
return value;
}
public Encoding getEncoding() {
return encoding;
}
}

View File

@ -0,0 +1,6 @@
package com.iheartradio.m3u8;
public enum Format {
M3U,
EXT_M3U;
}

View File

@ -0,0 +1,6 @@
package com.iheartradio.m3u8;
interface IExtTagParser extends LineParser {
String getTag();
boolean hasData();
}

View File

@ -0,0 +1,5 @@
package com.iheartradio.m3u8;
interface IExtTagWriter extends SectionWriter {
String getTag();
}

View File

@ -0,0 +1,5 @@
package com.iheartradio.m3u8;
interface IParseState<T> {
T buildPlaylist() throws ParseException;
}

View File

@ -0,0 +1,10 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.Playlist;
import java.io.IOException;
interface IPlaylistParser {
Playlist parse() throws IOException, ParseException, PlaylistException;
boolean isAvailable();
}

View File

@ -0,0 +1,5 @@
package com.iheartradio.m3u8;
interface LineParser {
void parse(String line, ParseState state) throws ParseException;
}

View File

@ -0,0 +1,65 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.InputStream;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist;
class M3uParser extends BaseM3uParser {
M3uParser(InputStream inputStream, Encoding encoding) {
super(inputStream, encoding);
}
@Override
public Playlist parse() throws IOException, ParseException, PlaylistException {
validateAvailable();
final ParseState state = new ParseState(mEncoding);
final TrackLineParser trackLineParser = new TrackLineParser();
try {
state.setMedia();
while (mScanner.hasNext()) {
final String line = mScanner.next();
validateLine(line);
if (line.length() == 0 || isComment(line)) {
continue;
} else {
trackLineParser.parse(line, state);
}
}
Playlist playlist = new Playlist.Builder()
.withMediaPlaylist(new MediaPlaylist.Builder()
.withTracks(state.getMedia().tracks)
.build())
.build();
PlaylistValidation validation = PlaylistValidation.from(playlist);
if (validation.isValid()) {
return playlist;
} else {
throw new PlaylistException(mScanner.getInput(), validation.getErrors());
}
} catch (ParseException exception) {
exception.setInput(mScanner.getInput());
throw exception;
}
}
private void validateLine(final String line) throws ParseException {
if (!isComment(line)) {
if (line.length() != line.trim().length()) {
throw ParseException.create(ParseExceptionType.WHITESPACE_IN_TRACK, line, "" + line.length());
}
}
}
private boolean isComment(final String line) {
return line.indexOf(Constants.COMMENT_PREFIX) == 0;
}
}

View File

@ -0,0 +1,41 @@
package com.iheartradio.m3u8;
import java.io.InputStream;
import java.util.Locale;
import java.util.Scanner;
class M3uScanner {
private final Scanner mScanner;
private final boolean mSupportsByteOrderMark;
private final StringBuilder mInput = new StringBuilder();
private boolean mCheckedByteOrderMark;
M3uScanner(InputStream inputStream, Encoding encoding) {
mScanner = new Scanner(inputStream, encoding.value).useLocale(Locale.US).useDelimiter(Constants.PARSE_NEW_LINE);
mSupportsByteOrderMark = encoding.supportsByteOrderMark;
}
String getInput() {
return mInput.toString();
}
boolean hasNext() {
return mScanner.hasNext();
}
String next() throws ParseException {
String line = mScanner.next();
if (mSupportsByteOrderMark && !mCheckedByteOrderMark) {
if (!line.isEmpty() && line.charAt(0) == Constants.UNICODE_BOM) {
line = line.substring(1);
}
mCheckedByteOrderMark = true;
}
mInput.append(line).append("\n");
return line;
}
}

View File

@ -0,0 +1,18 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.OutputStream;
import com.iheartradio.m3u8.data.Playlist;
class M3uWriter extends Writer{
M3uWriter(OutputStream outputStream, Encoding encoding) {
super(outputStream, encoding);
}
@Override
void doWrite(Playlist playlist) throws IOException {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,53 @@
package com.iheartradio.m3u8;
import java.util.ArrayList;
import java.util.List;
import com.iheartradio.m3u8.data.IFrameStreamInfo;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.MediaData;
import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StartData;
import com.iheartradio.m3u8.data.StreamInfo;
class MasterParseState implements PlaylistParseState<MasterPlaylist> {
private List<String> mUnknownTags;
private StartData mStartData;
public final List<PlaylistData> playlists = new ArrayList<>();
public final List<IFrameStreamInfo> iFramePlaylists = new ArrayList<>();
public final List<MediaData> mediaData = new ArrayList<>();
public StreamInfo streamInfo;
public boolean isDefault;
public boolean isNotAutoSelect;
public void clearMediaDataState() {
isDefault = false;
isNotAutoSelect = false;
}
@Override
public PlaylistParseState<MasterPlaylist> setUnknownTags(final List<String> unknownTags) {
mUnknownTags = unknownTags;
return this;
}
@Override
public PlaylistParseState<MasterPlaylist> setStartData(final StartData startData) {
mStartData = startData;
return this;
}
@Override
public MasterPlaylist buildPlaylist() throws ParseException {
return new MasterPlaylist.Builder()
.withPlaylists(playlists)
.withIFramePlaylists(iFramePlaylists)
.withMediaData(mediaData)
.withUnknownTags(mUnknownTags)
.withStartData(mStartData)
.build();
}
}

View File

@ -0,0 +1,343 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class MasterPlaylistLineParser implements LineParser {
private final IExtTagParser mTagParser;
private final LineParser mLineParser;
MasterPlaylistLineParser(IExtTagParser parser) {
this(parser, new ExtLineParser(parser));
}
MasterPlaylistLineParser(IExtTagParser tagParser, LineParser lineParser) {
mTagParser = tagParser;
mLineParser = lineParser;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
if (state.isMedia()) {
throw ParseException.create(ParseExceptionType.MASTER_IN_MEDIA, mTagParser.getTag());
}
state.setMaster();
mLineParser.parse(line, state);
}
// master playlist tags
static final IExtTagParser EXT_X_MEDIA = new IExtTagParser() {
private final LineParser mLineParser = new MasterPlaylistLineParser(this);
private final Map<String, AttributeParser<MediaData.Builder>> HANDLERS = new HashMap<String, AttributeParser<MediaData.Builder>>();
{
HANDLERS.put(Constants.TYPE, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final MediaType type = MediaType.fromValue(attribute.value);
if (type == null) {
throw ParseException.create(ParseExceptionType.INVALID_MEDIA_TYPE, getTag(), attribute.toString());
} else {
builder.withType(type);
}
}
});
HANDLERS.put(Constants.URI, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
builder.withUri(ParseUtil.decodeUri(ParseUtil.parseQuotedString(attribute.value, getTag()), state.encoding));
}
});
HANDLERS.put(Constants.GROUP_ID, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final String groupId = ParseUtil.parseQuotedString(attribute.value, getTag());
if (groupId.isEmpty()) {
throw ParseException.create(ParseExceptionType.EMPTY_MEDIA_GROUP_ID, getTag(), attribute.toString());
} else {
builder.withGroupId(groupId);
}
}
});
HANDLERS.put(Constants.LANGUAGE, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
builder.withLanguage(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.ASSOCIATED_LANGUAGE, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
builder.withAssociatedLanguage(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.NAME, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final String name = ParseUtil.parseQuotedString(attribute.value, getTag());
if (name.isEmpty()) {
throw ParseException.create(ParseExceptionType.EMPTY_MEDIA_NAME, getTag(), attribute.toString());
} else {
builder.withName(name);
}
}
});
HANDLERS.put(Constants.DEFAULT, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
boolean isDefault = ParseUtil.parseYesNo(attribute, getTag());
builder.withDefault(isDefault);
state.getMaster().isDefault = isDefault;
if (isDefault) {
if (state.getMaster().isNotAutoSelect) {
throw ParseException.create(ParseExceptionType.AUTO_SELECT_DISABLED_FOR_DEFAULT, getTag(), attribute.toString());
}
builder.withAutoSelect(true);
}
}
});
HANDLERS.put(Constants.AUTO_SELECT, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final boolean isAutoSelect = ParseUtil.parseYesNo(attribute, getTag());
builder.withAutoSelect(isAutoSelect);
state.getMaster().isNotAutoSelect = !isAutoSelect;
if (state.getMaster().isDefault && !isAutoSelect) {
throw ParseException.create(ParseExceptionType.AUTO_SELECT_DISABLED_FOR_DEFAULT, getTag(), attribute.toString());
}
}
});
HANDLERS.put(Constants.FORCED, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
builder.withForced(ParseUtil.parseYesNo(attribute, getTag()));
}
});
HANDLERS.put(Constants.IN_STREAM_ID, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final String inStreamId = ParseUtil.parseQuotedString(attribute.value, getTag());
if (Constants.EXT_X_MEDIA_IN_STREAM_ID_PATTERN.matcher(inStreamId).matches()) {
builder.withInStreamId(inStreamId);
} else {
throw ParseException.create(ParseExceptionType.INVALID_MEDIA_IN_STREAM_ID, getTag(), attribute.toString());
}
}
});
HANDLERS.put(Constants.CHARACTERISTICS, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final String[] characteristicStrings = ParseUtil.parseQuotedString(attribute.value, getTag()).split(Constants.COMMA);
if (characteristicStrings.length == 0) {
throw ParseException.create(ParseExceptionType.EMPTY_MEDIA_CHARACTERISTICS, getTag(), attribute.toString());
} else {
builder.withCharacteristics(Arrays.asList(characteristicStrings));
}
}
});
HANDLERS.put(Constants.CHANNELS, new AttributeParser<MediaData.Builder>() {
@Override
public void parse(Attribute attribute, MediaData.Builder builder, ParseState state) throws ParseException {
final String[] channelsStrings = ParseUtil.parseQuotedString(attribute.value, getTag()).split(Constants.LIST_SEPARATOR);
if (channelsStrings.length == 0 || channelsStrings[0].isEmpty()) {
throw ParseException.create(ParseExceptionType.EMPTY_MEDIA_CHANNELS, getTag(), attribute.toString());
} else {
final int channelsCount = ParseUtil.parseInt(channelsStrings[0], getTag());
builder.withChannels(channelsCount);
}
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_MEDIA_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
mLineParser.parse(line, state);
final MediaData.Builder builder = new MediaData.Builder();
state.getMaster().clearMediaDataState();
ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
state.getMaster().mediaData.add(builder.build());
}
};
static final IExtTagParser EXT_X_I_FRAME_STREAM_INF = new IExtTagParser() {
private final LineParser mLineParser = new MasterPlaylistLineParser(this);
private final Map<String, AttributeParser<IFrameStreamInfo.Builder>> HANDLERS = makeExtStreamInfHandlers(getTag());
{
HANDLERS.put(Constants.URI, new AttributeParser<IFrameStreamInfo.Builder>() {
@Override
public void parse(Attribute attribute, IFrameStreamInfo.Builder builder, ParseState state) throws ParseException {
builder.withUri(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_I_FRAME_STREAM_INF_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
mLineParser.parse(line, state);
final IFrameStreamInfo.Builder builder = new IFrameStreamInfo.Builder();
ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
state.getMaster().iFramePlaylists.add(builder.build());
}
};
static final IExtTagParser EXT_X_STREAM_INF = new IExtTagParser() {
private final LineParser mLineParser = new MasterPlaylistLineParser(this);
private final Map<String, AttributeParser<StreamInfo.Builder>> HANDLERS = makeExtStreamInfHandlers(getTag());
{
HANDLERS.put(Constants.AUDIO, new AttributeParser<StreamInfo.Builder>() {
@Override
public void parse(Attribute attribute, StreamInfo.Builder builder, ParseState state) throws ParseException {
builder.withAudio(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.SUBTITLES, new AttributeParser<StreamInfo.Builder>() {
@Override
public void parse(Attribute attribute, StreamInfo.Builder builder, ParseState state) throws ParseException {
builder.withSubtitles(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.CLOSED_CAPTIONS, new AttributeParser<StreamInfo.Builder>() {
@Override
public void parse(Attribute attribute, StreamInfo.Builder builder, ParseState state) throws ParseException {
if (!attribute.value.equals(Constants.NO_CLOSED_CAPTIONS)) {
builder.withClosedCaptions(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_STREAM_INF_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
mLineParser.parse(line, state);
final StreamInfo.Builder builder = new StreamInfo.Builder();
ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
state.getMaster().streamInfo = builder.build();
}
};
static <T extends StreamInfoBuilder> Map<String, AttributeParser<T>> makeExtStreamInfHandlers(final String tag) {
final Map<String, AttributeParser<T>> handlers = new HashMap<>();
handlers.put(Constants.BANDWIDTH, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
builder.withBandwidth(ParseUtil.parseInt(attribute.value, tag));
}
});
handlers.put(Constants.AVERAGE_BANDWIDTH, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
builder.withAverageBandwidth(ParseUtil.parseInt(attribute.value, tag));
}
});
handlers.put(Constants.CODECS, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
final String[] characteristicStrings = ParseUtil.parseQuotedString(attribute.value, tag).split(Constants.COMMA);
if (characteristicStrings.length > 0) {
builder.withCodecs(Arrays.asList(characteristicStrings));
}
}
});
handlers.put(Constants.RESOLUTION, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
builder.withResolution(ParseUtil.parseResolution(attribute.value, tag));
}
});
handlers.put(Constants.FRAME_RATE, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
builder.withFrameRate(ParseUtil.parseFloat(attribute.value, tag));
}
});
handlers.put(Constants.VIDEO, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
builder.withVideo(ParseUtil.parseQuotedString(attribute.value, tag));
}
});
handlers.put(Constants.PROGRAM_ID, new AttributeParser<T>() {
@Override
public void parse(Attribute attribute, T builder, ParseState state) throws ParseException {
// deprecated
}
});
return handlers;
}
}

View File

@ -0,0 +1,372 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.iheartradio.m3u8.data.IFrameStreamInfo;
import com.iheartradio.m3u8.data.IStreamInfo;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.MediaData;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StreamInfo;
abstract class MasterPlaylistTagWriter extends ExtTagWriter {
@Override
public final void write(TagWriter tagWriter, Playlist playlist) throws IOException, ParseException {
if (playlist.hasMasterPlaylist()) {
doWrite(tagWriter, playlist, playlist.getMasterPlaylist());
}
}
public void doWrite(TagWriter tagWriter,Playlist playlist, MasterPlaylist masterPlaylist) throws IOException, ParseException {
tagWriter.writeTag(getTag());
}
// master playlist tags
static final IExtTagWriter EXT_X_MEDIA = new MasterPlaylistTagWriter() {
private final Map<String, AttributeWriter<MediaData>> HANDLERS = new HashMap<String, AttributeWriter<MediaData>>();
{
HANDLERS.put(Constants.TYPE, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return true;
};
@Override
public String write(MediaData mediaData) throws ParseException {
return mediaData.getType().getValue();
}
});
HANDLERS.put(Constants.URI, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return mediaData.hasUri();
};
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(mediaData.getUri(), getTag());
}
});
HANDLERS.put(Constants.GROUP_ID, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return true;
};
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(mediaData.getGroupId(), getTag());
}
});
HANDLERS.put(Constants.LANGUAGE, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return mediaData.hasLanguage();
};
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(mediaData.getLanguage(), getTag());
}
});
HANDLERS.put(Constants.ASSOCIATED_LANGUAGE, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return mediaData.hasAssociatedLanguage();
};
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(mediaData.getAssociatedLanguage(), getTag());
}
});
HANDLERS.put(Constants.NAME, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return true;
};
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(mediaData.getName(), getTag());
}
});
HANDLERS.put(Constants.DEFAULT, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return true;
}
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeYesNo(mediaData.isDefault());
}
});
HANDLERS.put(Constants.AUTO_SELECT, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return true;
}
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeYesNo(mediaData.isAutoSelect());
}
});
HANDLERS.put(Constants.FORCED, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return true;
}
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeYesNo(mediaData.isForced());
}
});
HANDLERS.put(Constants.IN_STREAM_ID, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return mediaData.hasInStreamId();
}
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(mediaData.getInStreamId(), getTag());
}
});
HANDLERS.put(Constants.CHARACTERISTICS, new AttributeWriter<MediaData>() {
@Override
public boolean containsAttribute(MediaData mediaData) {
return mediaData.hasCharacteristics();
}
@Override
public String write(MediaData mediaData) throws ParseException {
return WriteUtil.writeQuotedString(WriteUtil.join(mediaData.getCharacteristics(), Constants.COMMA), getTag());
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_MEDIA_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MasterPlaylist masterPlaylist) throws IOException, ParseException {
if (masterPlaylist.getMediaData().size() > 0) {
List<MediaData> mds = masterPlaylist.getMediaData();
for(MediaData md : mds) {
writeAttributes(tagWriter, md, HANDLERS);
}
}
}
};
static abstract class EXT_STREAM_INF<T extends IStreamInfo> extends MasterPlaylistTagWriter {
final Map<String, AttributeWriter<T>> HANDLERS = new HashMap<>();
{
HANDLERS.put(Constants.BANDWIDTH, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return true;
}
@Override
public String write(T streamInfo) {
return Integer.toString(streamInfo.getBandwidth());
}
});
HANDLERS.put(Constants.AVERAGE_BANDWIDTH, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return streamInfo.hasAverageBandwidth();
}
@Override
public String write(T streamInfo) {
return Integer.toString(streamInfo.getAverageBandwidth());
}
});
HANDLERS.put(Constants.CODECS, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return streamInfo.hasCodecs();
}
@Override
public String write(T streamInfo) throws ParseException {
return WriteUtil.writeQuotedString(WriteUtil.join(streamInfo.getCodecs(), Constants.COMMA), getTag());
}
});
HANDLERS.put(Constants.RESOLUTION, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return streamInfo.hasResolution();
}
@Override
public String write(T streamInfo) throws ParseException {
return WriteUtil.writeResolution(streamInfo.getResolution());
}
});
HANDLERS.put(Constants.FRAME_RATE, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return streamInfo.hasFrameRate();
}
@Override
public String write(T streamInfo) throws ParseException {
return String.valueOf(streamInfo.getFrameRate());
}
});
HANDLERS.put(Constants.VIDEO, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return streamInfo.hasVideo();
}
@Override
public String write(T streamInfo) throws ParseException {
return WriteUtil.writeQuotedString(streamInfo.getVideo(), getTag());
}
});
HANDLERS.put(Constants.PROGRAM_ID, new AttributeWriter<T>() {
@Override
public boolean containsAttribute(T streamInfo) {
return false;
}
@Override
public String write(T streamInfo) {
// deprecated
return "";
}
});
}
@Override
boolean hasData() {
return true;
}
public abstract void doWrite(TagWriter tagWriter, Playlist playlist, MasterPlaylist masterPlaylist) throws IOException, ParseException;
}
static final IExtTagWriter EXT_X_I_FRAME_STREAM_INF = new EXT_STREAM_INF<IFrameStreamInfo>() {
{
HANDLERS.put(Constants.URI, new AttributeWriter<IFrameStreamInfo>() {
@Override
public boolean containsAttribute(IFrameStreamInfo streamInfo) {
return true;
};
@Override
public String write(IFrameStreamInfo streamInfo) throws ParseException {
return WriteUtil.writeQuotedString(streamInfo.getUri(), getTag());
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_I_FRAME_STREAM_INF_TAG;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MasterPlaylist masterPlaylist) throws IOException, ParseException {
for(IFrameStreamInfo streamInfo : masterPlaylist.getIFramePlaylists()) {
writeAttributes(tagWriter, streamInfo, HANDLERS);
}
}
};
static final IExtTagWriter EXT_X_STREAM_INF = new EXT_STREAM_INF<StreamInfo>() {
{
HANDLERS.put(Constants.AUDIO, new AttributeWriter<StreamInfo>() {
@Override
public boolean containsAttribute(StreamInfo streamInfo) {
return streamInfo.hasAudio();
}
@Override
public String write(StreamInfo streamInfo) throws ParseException {
return WriteUtil.writeQuotedString(streamInfo.getAudio(), getTag());
}
});
HANDLERS.put(Constants.SUBTITLES, new AttributeWriter<StreamInfo>() {
@Override
public boolean containsAttribute(StreamInfo streamInfo) {
return streamInfo.hasSubtitles();
}
@Override
public String write(StreamInfo streamInfo) throws ParseException {
return WriteUtil.writeQuotedString(streamInfo.getSubtitles(), getTag());
}
});
HANDLERS.put(Constants.CLOSED_CAPTIONS, new AttributeWriter<StreamInfo>() {
@Override
public boolean containsAttribute(StreamInfo streamInfo) {
return streamInfo.hasClosedCaptions();
}
public String write(StreamInfo streamInfo) throws ParseException {
return WriteUtil.writeQuotedString(streamInfo.getClosedCaptions(), getTag());
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_STREAM_INF_TAG;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MasterPlaylist masterPlaylist) throws IOException, ParseException {
for(PlaylistData playlistData : masterPlaylist.getPlaylists()) {
if (playlistData.hasStreamInfo()) {
writeAttributes(tagWriter, playlistData.getStreamInfo(), HANDLERS);
tagWriter.writeLine(playlistData.getUri());
}
}
}
};
}

View File

@ -0,0 +1,63 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.*;
import java.util.ArrayList;
import java.util.List;
class MediaParseState implements PlaylistParseState<MediaPlaylist> {
private List<String> mUnknownTags;
private StartData mStartData;
public final List<TrackData> tracks = new ArrayList<>();
public Integer targetDuration;
public Integer mediaSequenceNumber;
public boolean isIframesOnly;
public PlaylistType playlistType;
public TrackInfo trackInfo;
public EncryptionData encryptionData;
public String programDateTime;
public boolean endOfList;
public boolean hasDiscontinuity;
public MapInfo mapInfo;
public ByteRange byteRange;
@Override
public PlaylistParseState<MediaPlaylist> setUnknownTags(final List<String> unknownTags) {
mUnknownTags = unknownTags;
return this;
}
@Override
public PlaylistParseState<MediaPlaylist> setStartData(final StartData startData) {
mStartData = startData;
return this;
}
@Override
public MediaPlaylist buildPlaylist() throws ParseException {
return new MediaPlaylist.Builder()
.withTracks(tracks)
.withUnknownTags(mUnknownTags)
.withTargetDuration(targetDuration == null ? maximumDuration(tracks, 0) : targetDuration)
.withIsIframesOnly(isIframesOnly)
.withStartData(mStartData)
.withIsOngoing(!endOfList)
.withMediaSequenceNumber(mediaSequenceNumber == null ? 0 : mediaSequenceNumber)
.withPlaylistType(playlistType)
.build();
}
private static int maximumDuration(List<TrackData> tracks, float minValue) {
float max = minValue;
for (final TrackData trackData : tracks) {
if (trackData.hasTrackInfo()) {
max = Math.max(max, trackData.getTrackInfo().duration);
}
}
return 0;
}
}

View File

@ -0,0 +1,466 @@
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;
class MediaPlaylistLineParser implements LineParser {
private final IExtTagParser tagParser;
private final LineParser lineParser;
MediaPlaylistLineParser(IExtTagParser parser) {
this(parser, new ExtLineParser(parser));
}
MediaPlaylistLineParser(IExtTagParser tagParser, LineParser lineParser) {
this.tagParser = tagParser;
this.lineParser = lineParser;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
if (state.isMaster()) {
throw ParseException.create(ParseExceptionType.MEDIA_IN_MASTER, tagParser.getTag());
}
state.setMedia();
lineParser.parse(line, state);
}
// media playlist tags
static final IExtTagParser EXT_X_ENDLIST = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_ENDLIST_TAG;
}
@Override
public boolean hasData() {
return false;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
ParseUtil.match(Constants.EXT_X_ENDLIST_PATTERN, line, getTag());
state.getMedia().endOfList = true;
}
};
static final IExtTagParser EXT_X_I_FRAMES_ONLY = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_I_FRAMES_ONLY_TAG;
}
@Override
public boolean hasData() {
return false;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
ParseUtil.match(Constants.EXT_X_I_FRAMES_ONLY_PATTERN, line, getTag());
if (state.getCompatibilityVersion() < 4) {
throw ParseException.create(ParseExceptionType.REQUIRES_PROTOCOL_VERSION_4_OR_HIGHER, getTag());
}
state.setIsIframesOnly();
}
};
static final IExtTagParser EXT_X_PLAYLIST_TYPE = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_PLAYLIST_TYPE_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_PLAYLIST_TYPE_PATTERN, line, getTag());
if (state.getMedia().playlistType != null) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
state.getMedia().playlistType = ParseUtil.parseEnum(matcher.group(1), PlaylistType.class, getTag());
}
};
static final IExtTagParser EXT_X_PROGRAM_DATE_TIME = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_PROGRAM_DATE_TIME_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_PROGRAM_DATE_TIME_PATTERN, line, getTag());
if (state.getMedia().programDateTime != null) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
state.getMedia().programDateTime = ParseUtil.parseDateTime(line,getTag());
}
};
static final IExtTagParser EXT_X_START = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
private final Map<String, AttributeParser<StartData.Builder>> HANDLERS = new HashMap<>();
{
HANDLERS.put(Constants.TIME_OFFSET, new AttributeParser<StartData.Builder>() {
@Override
public void parse(Attribute attribute, StartData.Builder builder, ParseState state) throws ParseException {
builder.withTimeOffset(ParseUtil.parseFloat(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.PRECISE, new AttributeParser<StartData.Builder>() {
@Override
public void parse(Attribute attribute, StartData.Builder builder, ParseState state) throws ParseException {
builder.withPrecise(ParseUtil.parseYesNo(attribute, getTag()));
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_START_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
final StartData.Builder builder = new StartData.Builder();
ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
final StartData startData = builder.build();
state.getMedia().setStartData(startData);
}
};
static final IExtTagParser EXT_X_TARGETDURATION = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_TARGETDURATION_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_TARGETDURATION_PATTERN, line, getTag());
if (state.getMedia().targetDuration != null) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
state.getMedia().targetDuration = ParseUtil.parseInt(matcher.group(1), getTag());
}
};
static final IExtTagParser EXT_X_MEDIA_SEQUENCE = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_MEDIA_SEQUENCE_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_MEDIA_SEQUENCE_PATTERN, line, getTag());
if (state.getMedia().mediaSequenceNumber != null) {
throw ParseException.create(ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES, getTag(), line);
}
state.getMedia().mediaSequenceNumber = ParseUtil.parseInt(matcher.group(1), getTag());
}
};
static final IExtTagParser EXT_X_ALLOW_CACHE = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_ALLOW_CACHE_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
// deprecated
}
};
// media segment tags
static final IExtTagParser EXTINF = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXTINF_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.EXTINF_PATTERN, line, getTag());
state.getMedia().trackInfo = new TrackInfo(ParseUtil.parseFloat(matcher.group(1), getTag()), matcher.group(2));
}
};
static final IExtTagParser EXT_X_DISCONTINUITY = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
@Override
public String getTag() {
return Constants.EXT_X_DISCONTINUITY_TAG;
}
@Override
public boolean hasData() {
return false;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
final Matcher matcher = ParseUtil.match(Constants.EXT_X_DISCONTINUITY_PATTERN, line, getTag());
state.getMedia().hasDiscontinuity = true;
}
};
static final IExtTagParser EXT_X_KEY = new IExtTagParser() {
private final LineParser lineParser = new MediaPlaylistLineParser(this);
private final Map<String, AttributeParser<EncryptionData.Builder>> HANDLERS = new HashMap<>();
{
HANDLERS.put(Constants.METHOD, new AttributeParser<EncryptionData.Builder>() {
@Override
public void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException {
final EncryptionMethod method = EncryptionMethod.fromValue(attribute.value);
if (method == null) {
throw ParseException.create(ParseExceptionType.INVALID_ENCRYPTION_METHOD, getTag(), attribute.toString());
} else {
builder.withMethod(method);
}
}
});
HANDLERS.put(Constants.URI, new AttributeParser<EncryptionData.Builder>() {
@Override
public void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException {
builder.withUri(ParseUtil.decodeUri(ParseUtil.parseQuotedString(attribute.value, getTag()), state.encoding));
}
});
HANDLERS.put(Constants.IV, new AttributeParser<EncryptionData.Builder>() {
@Override
public void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException {
final List<Byte> initializationVector = ParseUtil.parseHexadecimal(attribute.value, getTag());
if ((initializationVector.size() != Constants.IV_SIZE) &&
(initializationVector.size() != Constants.IV_SIZE_ALTERNATIVE)) {
throw ParseException.create(ParseExceptionType.INVALID_IV_SIZE, getTag(), attribute.toString());
}
builder.withInitializationVector(initializationVector);
}
});
HANDLERS.put(Constants.KEY_FORMAT, new AttributeParser<EncryptionData.Builder>() {
@Override
public void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException {
builder.withKeyFormat(ParseUtil.parseQuotedString(attribute.value, getTag()));
}
});
HANDLERS.put(Constants.KEY_FORMAT_VERSIONS, new AttributeParser<EncryptionData.Builder>() {
@Override
public void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException {
final String[] versionStrings = ParseUtil.parseQuotedString(attribute.value, getTag()).split(Constants.LIST_SEPARATOR);
final List<Integer> versions = new ArrayList<>();
for (String version : versionStrings) {
try {
versions.add(Integer.parseInt(version));
} catch (NumberFormatException exception) {
throw ParseException.create(ParseExceptionType.INVALID_KEY_FORMAT_VERSIONS, getTag(), attribute.toString());
}
}
builder.withKeyFormatVersions(versions);
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_KEY_TAG;
}
@Override
public boolean hasData() {
return true;
}
@Override
public void parse(String line, ParseState state) throws ParseException {
lineParser.parse(line, state);
final EncryptionData.Builder builder = new EncryptionData.Builder()
.withKeyFormat(Constants.DEFAULT_KEY_FORMAT)
.withKeyFormatVersions(Constants.DEFAULT_KEY_FORMAT_VERSIONS);
ParseUtil.parseAttributes(line, builder, state, HANDLERS, getTag());
final EncryptionData encryptionData = builder.build();
if (encryptionData.getMethod() != EncryptionMethod.NONE && encryptionData.getUri() == null) {
throw ParseException.create(ParseExceptionType.MISSING_ENCRYPTION_URI, getTag(), line);
}
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);
}
};
}

View File

@ -0,0 +1,405 @@
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;
abstract class MediaPlaylistTagWriter extends ExtTagWriter {
@Override
public final void write(TagWriter tagWriter, Playlist playlist) throws IOException, ParseException {
if (playlist.hasMediaPlaylist()) {
doWrite(tagWriter, playlist, playlist.getMediaPlaylist());
}
}
public void doWrite(TagWriter tagWriter,Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException, ParseException {
tagWriter.writeTag(getTag());
}
// media playlist tags
static final IExtTagWriter EXT_X_ENDLIST = new MediaPlaylistTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_ENDLIST_TAG;
}
@Override
boolean hasData() {
return false;
}
@Override
public void doWrite(TagWriter tagWriter,Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException {
if (!mediaPlaylist.isOngoing()) {
tagWriter.writeTag(getTag());
}
}
};
static final IExtTagWriter EXT_X_I_FRAMES_ONLY = new MediaPlaylistTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_I_FRAMES_ONLY_TAG;
}
@Override
boolean hasData() {
return false;
}
@Override
public void doWrite(TagWriter tagWriter,Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException {
if (mediaPlaylist.isIframesOnly()) {
tagWriter.writeTag(getTag());
}
}
};
static final IExtTagWriter EXT_X_PLAYLIST_TYPE = new MediaPlaylistTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_PLAYLIST_TYPE_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException {
if (mediaPlaylist.getPlaylistType() != null) {
tagWriter.writeTag(getTag(), mediaPlaylist.getPlaylistType().getValue());
}
}
};
static final IExtTagWriter EXT_X_START = new MediaPlaylistTagWriter() {
private final Map<String, AttributeWriter<StartData>> HANDLERS = new HashMap<String, AttributeWriter<StartData>>();
{
HANDLERS.put(Constants.TIME_OFFSET, new AttributeWriter<StartData>() {
@Override
public boolean containsAttribute(StartData attributes) {
return true;
}
@Override
public String write(StartData attributes) throws ParseException {
return Float.toString(attributes.getTimeOffset());
}
});
HANDLERS.put(Constants.PRECISE, new AttributeWriter<StartData>() {
@Override
public boolean containsAttribute(StartData attributes) {
return true;
}
@Override
public String write(StartData attributes) throws ParseException {
if (attributes.isPrecise()) {
return Constants.YES;
} else {
return Constants.NO;
}
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_START_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException, ParseException {
if (mediaPlaylist.hasStartData()) {
StartData startData = mediaPlaylist.getStartData();
writeAttributes(tagWriter, startData, HANDLERS);
}
}
};
static final IExtTagWriter EXT_X_TARGETDURATION = new MediaPlaylistTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_TARGETDURATION_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException, ParseException {
tagWriter.writeTag(getTag(), Integer.toString(mediaPlaylist.getTargetDuration()));
}
};
static final IExtTagWriter EXT_X_MEDIA_SEQUENCE = new MediaPlaylistTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_MEDIA_SEQUENCE_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException ,ParseException {
tagWriter.writeTag(getTag(), Integer.toString(mediaPlaylist.getMediaSequenceNumber()));
};
};
static final IExtTagWriter EXT_X_ALLOW_CACHE = new MediaPlaylistTagWriter() {
@Override
public String getTag() {
return Constants.EXT_X_ALLOW_CACHE_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist){
// deprecated
};
};
// media segment tags
static final SectionWriter MEDIA_SEGMENTS = new SectionWriter() {
@Override
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());
}
}
}
};
private static void writeExtinf(TagWriter tagWriter, Playlist playlist, TrackData trackData) throws IOException {
final StringBuilder builder = new StringBuilder();
if (playlist.getCompatibilityVersion() < 3) {
builder.append(Integer.toString((int) trackData.getTrackInfo().duration));
} else {
builder.append(Float.toString(trackData.getTrackInfo().duration));
}
builder.append(Constants.COMMA);
if (trackData.getTrackInfo().title != null) {
builder.append(trackData.getTrackInfo().title);
}
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>>();
private EncryptionData mEncryptionData;
{
HANDLERS.put(Constants.METHOD, new AttributeWriter<EncryptionData>() {
@Override
public boolean containsAttribute(EncryptionData attributes) {
return true;
}
@Override
public String write(EncryptionData encryptionData) {
return encryptionData.getMethod().getValue();
}
});
HANDLERS.put(Constants.URI, new AttributeWriter<EncryptionData>() {
@Override
public boolean containsAttribute(EncryptionData attributes) {
return true;
}
@Override
public String write(EncryptionData encryptionData) throws ParseException {
return WriteUtil.writeQuotedString(encryptionData.getUri(), getTag());
}
});
HANDLERS.put(Constants.IV, new AttributeWriter<EncryptionData>() {
@Override
public boolean containsAttribute(EncryptionData attribute) {
return attribute.hasInitializationVector();
}
@Override
public String write(EncryptionData encryptionData) {
return WriteUtil.writeHexadecimal(encryptionData.getInitializationVector());
}
});
HANDLERS.put(Constants.KEY_FORMAT, new AttributeWriter<EncryptionData>() {
@Override
public boolean containsAttribute(EncryptionData attributes) {
return true;
}
@Override
public String write(EncryptionData encryptionData) throws ParseException {
//TODO check for version 5
return WriteUtil.writeQuotedString(encryptionData.getKeyFormat(), getTag(), true);
}
});
HANDLERS.put(Constants.KEY_FORMAT_VERSIONS, new AttributeWriter<EncryptionData>() {
@Override
public boolean containsAttribute(EncryptionData attributes) {
return true;
}
@Override
public String write(EncryptionData encryptionData) throws ParseException {
//TODO check for version 5
return WriteUtil.writeQuotedString(WriteUtil.join(encryptionData.getKeyFormatVersions(), Constants.LIST_SEPARATOR), getTag(), true);
}
});
}
@Override
public String getTag() {
return Constants.EXT_X_KEY_TAG;
}
@Override
boolean hasData() {
return true;
}
@Override
public void doWrite(TagWriter tagWriter, Playlist playlist, MediaPlaylist mediaPlaylist) throws IOException, ParseException {
writeAttributes(tagWriter, mEncryptionData, HANDLERS);
}
void writeTrackData(TagWriter tagWriter, Playlist playlist, TrackData trackData) throws IOException, ParseException {
if (trackData != null && trackData.hasEncryptionData()) {
final EncryptionData encryptionData = trackData.getEncryptionData();
if (!encryptionData.equals(mEncryptionData)) {
mEncryptionData = encryptionData;
write(tagWriter, playlist);
}
}
}
}
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);
}
}
}
}
}

View File

@ -0,0 +1,65 @@
package com.iheartradio.m3u8;
/**
* Represents a syntactic error in the input that prevented further parsing.
*/
public class ParseException extends Exception {
private static final long serialVersionUID = -2217152001086567983L;
private final String mMessageSuffix;
public final ParseExceptionType type;
private String mInput;
static ParseException create(ParseExceptionType type, String tag) {
return create(type, tag, null);
}
static ParseException create(ParseExceptionType type, String tag, String context) {
final StringBuilder builder = new StringBuilder();
if (tag != null) {
builder.append(tag);
}
if (context != null) {
if (builder.length() > 0) {
builder.append(" - ");
}
builder.append(context);
}
if (builder.length() > 0) {
return new ParseException(type, builder.toString());
} else {
return new ParseException(type);
}
}
ParseException(ParseExceptionType type) {
this(type, null);
}
ParseException(ParseExceptionType type, String messageSuffix) {
this.type = type;
mMessageSuffix = messageSuffix;
}
public String getInput() {
return mInput;
}
void setInput(String input) {
mInput = input;
}
public String getMessage() {
if (mMessageSuffix == null) {
return type.message;
} else {
return type.message + ": " + mMessageSuffix;
}
}
}

View File

@ -0,0 +1,50 @@
package com.iheartradio.m3u8;
public enum ParseExceptionType {
AUTO_SELECT_DISABLED_FOR_DEFAULT("default media data must be auto selected"),
BAD_EXT_TAG_FORMAT("bad format found for an EXT tag"),
EMPTY_MEDIA_CHANNELS("CHANNELS is empty"),
EMPTY_MEDIA_CHARACTERISTICS("CHARACTERISTICS is empty"),
EMPTY_MEDIA_GROUP_ID("GROUP-ID is empty"),
EMPTY_MEDIA_NAME("NAME is empty"),
ILLEGAL_WHITESPACE("found illegal whitespace"),
INTERNAL_ERROR("there was an unrecoverable problem"),
INVALID_ATTRIBUTE_NAME("invalid attribute name"),
INVALID_COMPATIBILITY_VERSION("invalid compatibility version"),
INVALID_ENCRYPTION_METHOD("invalid encryption method"),
INVALID_HEXADECIMAL_STRING("a hexadecimal string was not properly formatted"),
INVALID_IV_SIZE("the initialization vector is the wrong size"),
INVALID_KEY_FORMAT_VERSIONS("invalid KEYFORMATVERSIONS"),
INVALID_MEDIA_IN_STREAM_ID("invalid media INSTREAM-ID"),
INVALID_MEDIA_TYPE("invalid media TYPE"),
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"),
MISSING_ATTRIBUTE_VALUE("missing the value of an attribute"),
MISSING_ATTRIBUTE_SEPARATOR("missing the separator in an attribute"),
MISSING_ENCRYPTION_URI("missing the URI for encrypted media segments"),
MISSING_EXT_TAG_SEPARATOR("missing the colon after an EXT tag"),
MISSING_TRACK_INFO("missing EXTINF for a track in an extended media playlist"),
MULTIPLE_ATTRIBUTE_NAME_INSTANCES("multiple instances of an attribute name found in an attribute list"),
MULTIPLE_EXT_TAG_INSTANCES("multiple instances of an EXT tag found for which only one is allowed"),
NOT_JAVA_INTEGER("only java integers are supported"),
NOT_JAVA_ENUM("only specific values are supported"),
NOT_JAVA_FLOAT("only java floats are supported"),
NOT_YES_OR_NO("the only valid values are YES and NO"),
UNCLOSED_QUOTED_STRING("a quoted string was not closed"),
UNKNOWN_PLAYLIST_TYPE("unable to determine playlist type"),
UNSUPPORTED_COMPATIBILITY_VERSION("open m3u8 does not support this version"),
UNSUPPORTED_EXT_TAG_DETECTED("unsupported ext tag detected"),
WHITESPACE_IN_TRACK("whitespace was found surrounding a track"),
REQUIRES_PROTOCOL_VERSION_4_OR_HIGHER("A Media Playlist REQUIRES protocol version 4 or higher");
final String message;
private ParseExceptionType(String message) {
this.message = message;
}
}

View File

@ -0,0 +1,107 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.StartData;
import java.util.ArrayList;
import java.util.List;
class ParseState implements IParseState<Playlist> {
static final int NONE = -1;
public final Encoding encoding;
public final List<String> unknownTags = new ArrayList<>();
private MasterParseState mMasterParseState;
private MediaParseState mMediaParseState;
private boolean mIsExtended;
private int mCompatibilityVersion = NONE;
public StartData startData;
public ParseState(Encoding encoding) {
this.encoding = encoding;
}
public boolean isMaster() {
return mMasterParseState != null;
}
public MasterParseState getMaster() {
return mMasterParseState;
}
public void setMaster() throws ParseException {
if (isMedia()) {
throw new ParseException(ParseExceptionType.MASTER_IN_MEDIA);
}
if (mMasterParseState == null) {
mMasterParseState = new MasterParseState();
}
}
public boolean isMedia() {
return mMediaParseState != null;
}
public MediaParseState getMedia() {
return mMediaParseState;
}
public void setMedia() throws ParseException {
if (mMediaParseState == null) {
mMediaParseState = new MediaParseState();
}
}
public boolean isExtended() {
return mIsExtended;
}
public void setExtended() {
mIsExtended = true;
}
public void setIsIframesOnly() throws ParseException {
if (isMaster()) {
throw new ParseException(ParseExceptionType.MEDIA_IN_MASTER);
}
getMedia().isIframesOnly = true;
}
public int getCompatibilityVersion() {
return mCompatibilityVersion;
}
public void setCompatibilityVersion(int compatibilityVersion) {
mCompatibilityVersion = compatibilityVersion;
}
@Override
public Playlist buildPlaylist() throws ParseException {
final Playlist.Builder playlistBuilder = new Playlist.Builder();
if (isMaster()) {
playlistBuilder.withMasterPlaylist(buildInnerPlaylist(getMaster()));
} else if (isMedia()) {
playlistBuilder
.withMediaPlaylist(buildInnerPlaylist(getMedia()))
.withExtended(mIsExtended);
} else {
throw new ParseException(ParseExceptionType.UNKNOWN_PLAYLIST_TYPE);
}
return playlistBuilder
.withCompatibilityVersion(mCompatibilityVersion == NONE ? Playlist.MIN_COMPATIBILITY_VERSION : mCompatibilityVersion)
.build();
}
private <T> T buildInnerPlaylist(PlaylistParseState<T> innerParseState) throws ParseException {
return innerParseState
.setUnknownTags(unknownTags)
.setStartData(startData)
.buildPlaylist();
}
}

View File

@ -0,0 +1,265 @@
package com.iheartradio.m3u8;
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.regex.Matcher;
import java.util.regex.Pattern;
import com.iheartradio.m3u8.data.ByteRange;
import com.iheartradio.m3u8.data.Resolution;
final class ParseUtil {
public static ParsingMode parsingMode;
public static int parseInt(String string, String tag) throws ParseException {
try {
return Integer.parseInt(string);
} catch (NumberFormatException exception) {
throw ParseException.create(ParseExceptionType.NOT_JAVA_INTEGER, tag, string);
}
}
public static <T extends Enum<T>> T parseEnum(String string, Class<T> enumType, String tag) throws ParseException {
try {
return Enum.valueOf(enumType, string);
} catch (IllegalArgumentException exception) {
throw ParseException.create(ParseExceptionType.NOT_JAVA_ENUM, tag, string);
}
}
public static String parseDateTime(String string, String tag) throws ParseException {
Matcher matcher = Constants.EXT_X_PROGRAM_DATE_TIME_PATTERN.matcher(string);
if (!matcher.matches()) {
throw new ParseException(ParseExceptionType.INVALID_DATE_TIME_FORMAT, tag);
}
return matcher.group(1);
}
public static float parseFloat(String string, String tag) throws ParseException {
try {
return Float.parseFloat(string);
} catch (NumberFormatException exception) {
throw ParseException.create(ParseExceptionType.NOT_JAVA_FLOAT, tag, string);
}
}
public static List<Byte> parseHexadecimal(String hexString, String tag) throws ParseException {
final List<Byte> bytes = new ArrayList<Byte>();
final Matcher matcher = Constants.HEXADECIMAL_PATTERN.matcher(hexString.toUpperCase(Locale.US));
if (matcher.matches()) {
String valueString = matcher.group(1);
if (valueString.length() % 2 != 0) {
throw ParseException.create(ParseExceptionType.INVALID_HEXADECIMAL_STRING, tag, hexString);
}
for (int i = 0; i < valueString.length(); i += 2) {
bytes.add((byte)(Short.parseShort(valueString.substring(i, i+2), 16) & 0xFF));
}
return bytes;
} else {
throw ParseException.create(ParseExceptionType.INVALID_HEXADECIMAL_STRING, tag, hexString);
}
}
private static byte hexCharToByte(char hex) {
if (hex >= 'A') {
return (byte) ((hex & 0xF) + 9);
} else {
return (byte) (hex & 0xF);
}
}
public static boolean parseYesNo(Attribute attribute, String tag) throws ParseException {
if (attribute.value.equals(Constants.YES)) {
return true;
} else if (attribute.value.equals(Constants.NO)) {
return false;
} else {
throw ParseException.create(ParseExceptionType.NOT_YES_OR_NO, tag, attribute.toString());
}
}
public static Resolution parseResolution(String resolutionString, String tag) throws ParseException {
Matcher matcher = Constants.RESOLUTION_PATTERN.matcher(resolutionString);
if (!matcher.matches()) {
throw new ParseException(ParseExceptionType.INVALID_RESOLUTION_FORMAT, tag);
}
return new Resolution(parseInt(matcher.group(1), tag), parseInt(matcher.group(2), tag));
}
public static String parseQuotedString(String quotedString, String tag) throws ParseException {
final StringBuilder builder = new StringBuilder();
boolean isEscaping = false;
int quotesFound = 0;
for (int i = 0; i < quotedString.length(); ++i) {
final char c = quotedString.charAt(i);
if (i == 0 && c != '"') {
if (isWhitespace(c)) {
throw new ParseException(ParseExceptionType.ILLEGAL_WHITESPACE, tag);
} else {
throw new ParseException(ParseExceptionType.INVALID_QUOTED_STRING, tag);
}
} else if (quotesFound == 2) {
if (isWhitespace(c)) {
throw new ParseException(ParseExceptionType.ILLEGAL_WHITESPACE, tag);
} else {
throw new ParseException(ParseExceptionType.INVALID_QUOTED_STRING, tag);
}
} else if (i == quotedString.length() - 1) {
if (c != '"' || isEscaping) {
throw new ParseException(ParseExceptionType.UNCLOSED_QUOTED_STRING, tag);
}
} else {
if (isEscaping) {
builder.append(c);
isEscaping = false;
} else {
if (c == '\\') {
isEscaping = true;
} else if (c == '"') {
++quotesFound;
} else {
builder.append(c);
}
}
}
}
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';
}
public static String decodeUri(String encodedUri, Encoding encoding) throws ParseException {
try {
return URLDecoder.decode(encodedUri.replace("+", "%2B"), encoding.value);
} catch (UnsupportedEncodingException exception) {
throw new ParseException(ParseExceptionType.INTERNAL_ERROR);
}
}
public static Matcher match(Pattern pattern, String line, String tag) throws ParseException {
final Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
throw ParseException.create(ParseExceptionType.BAD_EXT_TAG_FORMAT, tag, line);
}
return matcher;
}
public static <T> void parseAttributes(String line, T builder, ParseState state, Map<String, ? extends AttributeParser<T>> handlers, String tag) throws ParseException {
for (Attribute attribute : parseAttributeList(line, tag)) {
if (handlers.containsKey(attribute.name)) {
handlers.get(attribute.name).parse(attribute, builder, state);
} else {
if(parsingMode != null && !parsingMode.allowUnkownAttributes) {
throw ParseException.create(ParseExceptionType.INVALID_ATTRIBUTE_NAME, tag, line);
}
}
}
}
public static List<Attribute> parseAttributeList(String line, String tag) throws ParseException {
final List<Attribute> attributes = new ArrayList<Attribute>();
final Set<String> attributeNames = new HashSet<String>();
for (String string : splitAttributeList(line, tag)) {
final int separator = string.indexOf(Constants.ATTRIBUTE_SEPARATOR);
final int quote = string.indexOf("\"");
if (separator == -1 || (quote != -1 && quote < separator)) {
throw ParseException.create(ParseExceptionType.MISSING_ATTRIBUTE_SEPARATOR, tag, attributes.toString());
} else {
//Even Apple playlists have sometimes spaces after a ,
final String name = string.substring(0, separator).trim();
final String value = string.substring(separator + 1);
if (name.isEmpty()) {
throw ParseException.create(ParseExceptionType.MISSING_ATTRIBUTE_NAME, tag, attributes.toString());
}
if (value.isEmpty()) {
throw ParseException.create(ParseExceptionType.MISSING_ATTRIBUTE_VALUE, tag, attributes.toString());
}
if (!attributeNames.add(name)) {
throw ParseException.create(ParseExceptionType.MULTIPLE_ATTRIBUTE_NAME_INSTANCES, tag, attributes.toString());
}
attributes.add(new Attribute(name, value));
}
}
return attributes;
}
public static List<String> splitAttributeList(String line, String tag) throws ParseException {
final List<Integer> splitIndices = new ArrayList<Integer>();
final List<String> attributes = new ArrayList<String>();
int startIndex = line.indexOf(Constants.EXT_TAG_END) + 1;
boolean isQuotedString = false;
boolean isEscaping = false;
for (int i = startIndex; i < line.length(); i++) {
if (isQuotedString) {
if (isEscaping) {
isEscaping = false;
} else {
char c = line.charAt(i);
if (c == '\\') {
isEscaping = true;
} else if (c == '"') {
isQuotedString = false;
}
}
} else {
char c = line.charAt(i);
if (c == Constants.COMMA_CHAR) {
splitIndices.add(i);
} else if (c == '"') {
isQuotedString = true;
}
}
}
if (isQuotedString) {
throw new ParseException(ParseExceptionType.UNCLOSED_QUOTED_STRING, tag);
}
for (Integer splitIndex : splitIndices) {
attributes.add(line.substring(startIndex, splitIndex));
startIndex = splitIndex + 1;
}
attributes.add(line.substring(startIndex));
return attributes;
}
}

View File

@ -0,0 +1,66 @@
package com.iheartradio.m3u8;
public class ParsingMode {
public static final ParsingMode STRICT = new Builder().build();
public static final ParsingMode LENIENT = new Builder()
.allowUnknownTags()
.allowNegativeNumbers()
.allowUnkownAttributes()
.build();
/**
* If true, unrecognized tags will not throw an exception. Instead, they will be made available in the playlist for custom parsing.
*/
public final boolean allowUnknownTags;
/**
* If true, negative numbers in violation of the specification will not throw an exception.
*/
public final boolean allowNegativeNumbers;
/**
* If true, negative numbers in violation of the specification will not throw an exception.
*/
public final boolean allowUnkownAttributes;
private ParsingMode(final boolean allowUnknownTags, final boolean allowNegativeNumbers, final boolean allowUnkownAttributes) {
this.allowUnknownTags = allowUnknownTags;
this.allowNegativeNumbers = allowNegativeNumbers;
this.allowUnkownAttributes = allowUnkownAttributes;
}
public static class Builder {
private boolean mAllowUnknownTags = false;
private boolean mAllowNegativeNumbers = false;
private boolean allowUnkownAttributes = false;
/**
* Call to prevent throwing an exception when parsing unrecognized tags.
*/
public Builder allowUnknownTags() {
mAllowUnknownTags = true;
return this;
}
/**
* Call to prevent throwing an exception when parsing negative numbers in violation of the specification.
*/
public Builder allowNegativeNumbers() {
mAllowNegativeNumbers = true;
return this;
}
/**
* Call to prevent throwing an exception when parsing unkown attributes in EXT-X-STREAM-INF.
*/
public Builder allowUnkownAttributes() {
allowUnkownAttributes = true;
return this;
}
public ParsingMode build() {
return new ParsingMode(mAllowUnknownTags, mAllowNegativeNumbers, allowUnkownAttributes);
}
}
}

View File

@ -0,0 +1,132 @@
package com.iheartradio.m3u8;
public enum PlaylistError {
NO_PLAYLIST,
/**
* Compatibility version cannot be less than Playlist.MIN_COMPATIBILITY_VERSION.
*/
COMPATIBILITY_TOO_LOW,
/**
* A master or media playlist is required.
*/
NO_MASTER_OR_MEDIA,
/**
* Having both a master and a media playlist is not allowed.
*/
BOTH_MASTER_AND_MEDIA,
/**
* A master playlist must use the extend M3U tags.
*/
MASTER_NOT_EXTENDED,
/**
* StartData requires a time offset.
*/
START_DATA_WITHOUT_TIME_OFFSET,
/**
* EncryptionData requires a method.
*/
ENCRYPTION_DATA_WITHOUT_METHOD,
/**
* MediaData requires a type.
*/
MEDIA_DATA_WITHOUT_TYPE,
/**
* MediaData requires a group ID.
*/
MEDIA_DATA_WITHOUT_GROUP_ID,
/**
* MediaData requires a name.
*/
MEDIA_DATA_WITHOUT_NAME,
/**
* Close captions MediaData cannot have a URI.
*/
CLOSE_CAPTIONS_WITH_URI,
/**
* Close captions MediaData requires in stream ID.
*/
CLOSE_CAPTIONS_WITHOUT_IN_STREAM_ID,
/**
* MediaData can only have in stream ID if it is close captions.
*/
IN_STREAM_ID_WITHOUT_CLOSE_CAPTIONS,
/**
* MediaData must be auto selected if it is default.
*/
DEFAULT_WITHOUT_AUTO_SELECT,
/**
* Only subtitles MediaData can be forced.
*/
FORCED_WITHOUT_SUBTITLES,
/**
* TrackData requires a location.
*/
TRACK_DATA_WITHOUT_URI,
/**
* TrackData requires TrackInfo for playlists that use extended M3U tags.
*/
EXTENDED_TRACK_DATA_WITHOUT_TRACK_INFO,
/**
* TrackInfo duration must be non-nagative.
* @see com.iheartradio.m3u8.ParsingMode#allowNegativeNumbers
*/
TRACK_INFO_WITH_NEGATIVE_DURATION,
/**
* PlaylistData requires a URI.
*/
PLAYLIST_DATA_WITHOUT_URI,
/**
* StreamInfo requires a bandwidth.
*/
STREAM_INFO_WITH_NO_BANDWIDTH,
/**
* The average bandwidth in StreamInfo must be non-negative or StreamInfo.NO_BANDWIDTH.
*/
STREAM_INFO_WITH_INVALID_AVERAGE_BANDWIDTH,
/**
* IFrameStreamInfo requires a URI.
*/
I_FRAME_STREAM_WITHOUT_URI,
/**
* IFrameStreamInfo requires a bandwidth.
*/
I_FRAME_STREAM_WITH_NO_BANDWIDTH,
/**
* 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,
}

View File

@ -0,0 +1,26 @@
package com.iheartradio.m3u8;
import java.util.Set;
/**
* Represents a playlist with invalid data.
*/
public class PlaylistException extends Exception {
private static final long serialVersionUID = 7426782115004559238L;
private final String mInput;
private final Set<PlaylistError> mErrors;
public PlaylistException(String input, Set<PlaylistError> errors) {
mInput = input;
mErrors = errors;
}
public String getInput() {
return mInput;
}
public Set<PlaylistError> getErrors() {
return mErrors;
}
}

View File

@ -0,0 +1,18 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.PlaylistData;
class PlaylistLineParser implements LineParser {
@Override
public void parse(String line, ParseState state) {
final PlaylistData.Builder builder = new PlaylistData.Builder();
final MasterParseState masterState = state.getMaster();
masterState.playlists.add(builder
.withUri(line)
.withStreamInfo(masterState.streamInfo)
.build());
masterState.streamInfo = null;
}
}

View File

@ -0,0 +1,11 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.StartData;
import java.util.List;
interface PlaylistParseState<T> extends IParseState<T> {
PlaylistParseState<T> setUnknownTags(List<String> unknownTags);
PlaylistParseState<T> setStartData(StartData startData);
}

View File

@ -0,0 +1,148 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.InputStream;
import com.iheartradio.m3u8.data.Playlist;
public class PlaylistParser implements IPlaylistParser {
private final IPlaylistParser mPlaylistParser;
/**
* Equivalent to:
* <pre>
* new PlaylistParser(inputStream, format, filename, ParsingMode.STRICT);
* </pre>
* @param inputStream an open input stream positioned at the beginning of the file
* @param format requires the playlist to be this format
* @param filename the extension of this filename will be used to determine the encoding required of the playlist
*/
public PlaylistParser(InputStream inputStream, Format format, String filename) {
this(inputStream, format, parseExtension(filename), ParsingMode.STRICT);
}
/**
* @param inputStream an open input stream positioned at the beginning of the file
* @param format requires the playlist to be this format
* @param filename the extension of this filename will be used to determine the encoding required of the playlist
* @param parsingMode indicates how to handle unknown lines in the input stream
*/
public PlaylistParser(InputStream inputStream, Format format, String filename, ParsingMode parsingMode) {
this(inputStream, format, parseExtension(filename), parsingMode);
}
/**
* Equivalent to:
* <pre>
* new PlaylistParser(inputStream, format, extension, ParsingMode.STRICT);
* </pre>
* @param inputStream an open input stream positioned at the beginning of the file
* @param format requires the playlist to be this format
* @param extension requires the playlist be encoded according to this extension {M3U : windows-1252, M3U8 : utf-8}
*/
public PlaylistParser(InputStream inputStream, Format format, Extension extension) {
this(inputStream, format, extension.encoding, ParsingMode.STRICT);
}
/**
* @param inputStream an open input stream positioned at the beginning of the file
* @param format requires the playlist to be this format
* @param extension requires the playlist be encoded according to this extension {M3U : windows-1252, M3U8 : utf-8}
* @param parsingMode indicates how to handle unknown lines in the input stream
*/
public PlaylistParser(InputStream inputStream, Format format, Extension extension, ParsingMode parsingMode) {
this(inputStream, format, extension.encoding, parsingMode);
}
/**
* Equivalent to:
* <pre>
* new PlaylistParser(inputStream, format, encoding, ParsingMode.STRICT);
* </pre>
* @param inputStream an open input stream positioned at the beginning of the file
* @param format requires the playlist to be this format
* @param encoding required encoding for the playlist
*/
public PlaylistParser(InputStream inputStream, Format format, Encoding encoding) {
this(inputStream, format, encoding, ParsingMode.STRICT);
}
/**
* @param inputStream an open input stream positioned at the beginning of the file
* @param format requires the playlist to be this format
* @param encoding required encoding for the playlist
* @param parsingMode indicates how to handle unknown lines in the input stream
*/
public PlaylistParser(InputStream inputStream, Format format, Encoding encoding, ParsingMode parsingMode) {
if (inputStream == null) {
throw new IllegalArgumentException("inputStream is null");
}
if (format == null) {
throw new IllegalArgumentException("format is null");
}
if (encoding == null) {
throw new IllegalArgumentException("encoding is null");
}
if (parsingMode == null && format != Format.M3U) {
throw new IllegalArgumentException("parsingMode is null");
}
ParseUtil.parsingMode = parsingMode;
switch (format) {
case M3U:
mPlaylistParser = new M3uParser(inputStream, encoding);
break;
case EXT_M3U:
mPlaylistParser = new ExtendedM3uParser(inputStream, encoding, parsingMode);
break;
default:
throw new RuntimeException("unsupported format detected, this should be impossible: " + format);
}
}
/**
* This will not close the InputStream.
* @return Playlist which is either a MasterPlaylist or a MediaPlaylist, this will never return null
* @throws IOException if the InputStream throws an IOException
* @throws java.io.EOFException if there is no data to parse
* @throws ParseException if there is a syntactic error in the playlist
* @throws PlaylistException if the data in the parsed playlist is invalid
*/
@Override
public Playlist parse() throws IOException, ParseException, PlaylistException {
return mPlaylistParser.parse();
}
/**
* @return true if there is more data to parse, false otherwise
*/
@Override
public boolean isAvailable() {
return mPlaylistParser.isAvailable();
}
private static Extension parseExtension(String filename) {
if (filename == null) {
throw new IllegalArgumentException("filename is null");
}
int index = filename.lastIndexOf(".");
if (index == -1) {
throw new IllegalArgumentException("filename has no extension: " + filename);
} else {
final String extension = filename.substring(index + 1);
if (Extension.M3U.value.equalsIgnoreCase(extension)) {
return Extension.M3U;
} else if (Extension.M3U8.value.equalsIgnoreCase(extension)) {
return Extension.M3U8;
} else {
throw new IllegalArgumentException("filename extension should be .m3u or .m3u8: " + filename);
}
}
}
}

View File

@ -0,0 +1,224 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class PlaylistValidation {
private final Set<PlaylistError> mErrors;
private PlaylistValidation(Set<PlaylistError> errors) {
mErrors = Collections.unmodifiableSet(errors);
}
@Override
public String toString() {
return new StringBuilder()
.append("(PlaylistValidation")
.append(" valid=").append(isValid())
.append(" errors=").append(mErrors)
.append(")")
.toString();
}
public boolean isValid() {
return mErrors.isEmpty();
}
public Set<PlaylistError> getErrors() {
return mErrors;
}
/**
* Equivalent to: PlaylistValidation.from(playlist, ParsingMode.STRICT)
*/
public static PlaylistValidation from(Playlist playlist) {
return PlaylistValidation.from(playlist, ParsingMode.STRICT);
}
public static PlaylistValidation from(Playlist playlist, ParsingMode parsingMode) {
Set<PlaylistError> errors = new HashSet<>();
if (playlist == null) {
errors.add(PlaylistError.NO_PLAYLIST);
return new PlaylistValidation(errors);
}
if (playlist.getCompatibilityVersion() < Playlist.MIN_COMPATIBILITY_VERSION) {
errors.add(PlaylistError.COMPATIBILITY_TOO_LOW);
}
if (hasNoPlaylistTypes(playlist)) {
errors.add(PlaylistError.NO_MASTER_OR_MEDIA);
} else if (hasBothPlaylistTypes(playlist)) {
errors.add(PlaylistError.BOTH_MASTER_AND_MEDIA);
}
if (playlist.hasMasterPlaylist()) {
if (!playlist.isExtended()) {
errors.add(PlaylistError.MASTER_NOT_EXTENDED);
}
addMasterPlaylistErrors(playlist.getMasterPlaylist(), errors);
}
if (playlist.hasMediaPlaylist()) {
addMediaPlaylistErrors(playlist.getMediaPlaylist(), errors, playlist.isExtended(), parsingMode);
}
return new PlaylistValidation(errors);
}
private static boolean hasNoPlaylistTypes(Playlist playlist) {
return !(playlist.hasMasterPlaylist() || playlist.hasMediaPlaylist());
}
private static boolean hasBothPlaylistTypes(Playlist playlist) {
return playlist.hasMasterPlaylist() && playlist.hasMediaPlaylist();
}
private static void addMasterPlaylistErrors(MasterPlaylist playlist, Set<PlaylistError> errors) {
for (PlaylistData playlistData : playlist.getPlaylists()) {
addPlaylistDataErrors(playlistData, errors);
}
for (IFrameStreamInfo iFrameStreamInfo : playlist.getIFramePlaylists()) {
addIFrameStreamInfoErrors(iFrameStreamInfo, errors);
}
for (MediaData mediaData : playlist.getMediaData()) {
addMediaDataErrors(mediaData, errors);
}
}
private static void addMediaPlaylistErrors(MediaPlaylist playlist, Set<PlaylistError> errors, boolean isExtended, ParsingMode parsingMode) {
if (isExtended && playlist.hasStartData()) {
addStartErrors(playlist.getStartData(), errors);
}
addByteRangeErrors(playlist.getTracks(), errors, parsingMode);
for (TrackData trackData : playlist.getTracks()) {
addTrackDataErrors(trackData, errors, isExtended, parsingMode);
}
}
private static void addByteRangeErrors(List<TrackData> tracks, Set<PlaylistError> errors, ParsingMode parsingMode) {
Set<String> knownOffsets = new HashSet<>();
for (TrackData track : tracks) {
if (!track.hasByteRange()) {
continue;
}
if (track.getByteRange().hasOffset()) {
knownOffsets.add(track.getUri());
} else if (!knownOffsets.contains(track.getUri())) {
errors.add(PlaylistError.BYTERANGE_WITH_UNDEFINED_OFFSET);
}
}
}
private static void addStartErrors(StartData startData, Set<PlaylistError> errors) {
if (Float.isNaN(startData.getTimeOffset())) {
errors.add(PlaylistError.START_DATA_WITHOUT_TIME_OFFSET);
}
}
private static void addPlaylistDataErrors(PlaylistData playlistData, Set<PlaylistError> errors) {
if (playlistData.getUri() == null || playlistData.getUri().isEmpty()) {
errors.add(PlaylistError.PLAYLIST_DATA_WITHOUT_URI);
}
if (playlistData.hasStreamInfo()) {
if (playlistData.getStreamInfo().getBandwidth() == StreamInfo.NO_BANDWIDTH) {
errors.add(PlaylistError.STREAM_INFO_WITH_NO_BANDWIDTH);
}
if (playlistData.getStreamInfo().getAverageBandwidth() < StreamInfo.NO_BANDWIDTH) {
errors.add(PlaylistError.STREAM_INFO_WITH_INVALID_AVERAGE_BANDWIDTH);
}
}
}
private static void addIFrameStreamInfoErrors(IFrameStreamInfo streamInfo, Set<PlaylistError> errors) {
if (streamInfo.getUri() == null || streamInfo.getUri().isEmpty()) {
errors.add(PlaylistError.I_FRAME_STREAM_WITHOUT_URI);
}
if (streamInfo.getBandwidth() == StreamInfo.NO_BANDWIDTH) {
errors.add(PlaylistError.I_FRAME_STREAM_WITH_NO_BANDWIDTH);
}
if (streamInfo.getAverageBandwidth() < StreamInfo.NO_BANDWIDTH) {
errors.add(PlaylistError.I_FRAME_STREAM_WITH_INVALID_AVERAGE_BANDWIDTH);
}
}
private static void addMediaDataErrors(MediaData mediaData, Set<PlaylistError> errors) {
if (mediaData.getType() == null) {
errors.add(PlaylistError.MEDIA_DATA_WITHOUT_TYPE);
}
if (mediaData.getGroupId() == null) {
errors.add(PlaylistError.MEDIA_DATA_WITHOUT_GROUP_ID);
}
if (mediaData.getName() == null) {
errors.add(PlaylistError.MEDIA_DATA_WITHOUT_NAME);
}
if (mediaData.getType() == MediaType.CLOSED_CAPTIONS) {
if (mediaData.hasUri()) {
errors.add(PlaylistError.CLOSE_CAPTIONS_WITH_URI);
}
if (mediaData.getInStreamId() == null) {
errors.add(PlaylistError.CLOSE_CAPTIONS_WITHOUT_IN_STREAM_ID);
}
} else {
if (mediaData.getType() != MediaType.CLOSED_CAPTIONS && mediaData.getInStreamId() != null) {
errors.add(PlaylistError.IN_STREAM_ID_WITHOUT_CLOSE_CAPTIONS);
}
}
if (mediaData.isDefault() && !mediaData.isAutoSelect()) {
errors.add(PlaylistError.DEFAULT_WITHOUT_AUTO_SELECT);
}
if (mediaData.getType() != MediaType.SUBTITLES && mediaData.isForced()) {
errors.add(PlaylistError.FORCED_WITHOUT_SUBTITLES);
}
}
private static void addTrackDataErrors(TrackData trackData, Set<PlaylistError> errors, boolean isExtended, ParsingMode parsingMode) {
if (trackData.getUri() == null || trackData.getUri().isEmpty()) {
errors.add(PlaylistError.TRACK_DATA_WITHOUT_URI);
}
if (isExtended && !trackData.hasTrackInfo()) {
errors.add(PlaylistError.EXTENDED_TRACK_DATA_WITHOUT_TRACK_INFO);
}
if (trackData.hasEncryptionData()) {
if (trackData.getEncryptionData().getMethod() == null) {
errors.add(PlaylistError.ENCRYPTION_DATA_WITHOUT_METHOD);
}
}
if (trackData.hasTrackInfo()) {
if (!parsingMode.allowNegativeNumbers && trackData.getTrackInfo().duration < 0) {
errors.add(PlaylistError.TRACK_INFO_WITH_NEGATIVE_DURATION);
}
}
if (trackData.hasMapInfo()) {
if (trackData.getMapInfo().getUri() == null || trackData.getMapInfo().getUri().isEmpty()) {
errors.add(PlaylistError.MAP_INFO_WITHOUT_URI);
}
}
}
}

View File

@ -0,0 +1,131 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.OutputStream;
import com.iheartradio.m3u8.data.Playlist;
import static com.iheartradio.m3u8.Constants.UTF_8_BOM_BYTES;
public class PlaylistWriter {
private final Writer mWriter;
private final OutputStream mOutputStream;
private final boolean mShouldWriteByteOrderMark;
private boolean mFirstWrite = true;
/**
* This writes the given Playlist to the given OutputStream with a prepended BOM (Byte Order Mark).
* This exists for convenience if you absolutely need a BOM.
* From the specification: "They (playlist files) MUST NOT contain any byte order mark (BOM); Clients SHOULD reject Playlists which contain a BOM..."
*
* @param outputStream OutputStream playlists will be written to
* @param format format in which to write the playlist
* @param encoding encoding in which to write the playlist
*/
public PlaylistWriter(OutputStream outputStream, Format format, Encoding encoding) {
this(outputStream, format, encoding, false);
}
private PlaylistWriter(OutputStream outputStream, Format format, Encoding encoding, boolean useByteOrderMark) {
if (outputStream == null) {
throw new IllegalArgumentException("outputStream is null");
}
if (format == null) {
throw new IllegalArgumentException("format is null");
}
if (encoding == null) {
throw new IllegalArgumentException("encoding is null");
}
mOutputStream = outputStream;
mShouldWriteByteOrderMark = encoding.supportsByteOrderMark && useByteOrderMark;
switch (format) {
case M3U:
mWriter = new M3uWriter(outputStream, encoding);
break;
case EXT_M3U:
mWriter = new ExtendedM3uWriter(outputStream, encoding);
break;
default:
throw new RuntimeException("unsupported format detected, this should be impossible: " + format);
}
}
/**
* Writes the given Playlist to the contained OutputStream.
*
* @throws IOException
* @throws ParseException if the data is improperly formatted
* @throws PlaylistException if the representation of the playlist is invalid,
* that is, if PlaylistValidation.from(playlist).isValid() == false
*/
public void write(Playlist playlist) throws IOException, ParseException, PlaylistException {
final PlaylistValidation validation = PlaylistValidation.from(playlist);
if (!validation.isValid()) {
throw new PlaylistException("", validation.getErrors());
}
writeByteOrderMark();
mWriter.write(playlist);
mFirstWrite = false;
}
private void writeByteOrderMark() throws IOException {
if (mShouldWriteByteOrderMark && mFirstWrite) {
for (int i = 0; i < UTF_8_BOM_BYTES.length; ++i) {
mOutputStream.write(UTF_8_BOM_BYTES[i]);
}
}
}
public static class Builder {
private OutputStream mOutputStream;
private Format mFormat;
private Encoding mEncoding;
private boolean mUseByteOrderMark;
/**
* @param outputStream OutputStream playlists will be written to
*/
public Builder withOutputStream(OutputStream outputStream) {
mOutputStream = outputStream;
return this;
}
/**
* @param format format in which to write the playlist
*/
public Builder withFormat(Format format) {
mFormat = format;
return this;
}
/**
* @param encoding encoding in which to write the playlist
*/
public Builder withEncoding(Encoding encoding) {
mEncoding = encoding;
return this;
}
/**
* When using a BOM, the first call to write will prepend the BOM character to the playlist. Subsequent
* calls to write will not prepend the BOM to support writing multiple ENDLIST delimited playlists.
*
* @deprecated The specification explicitly says that playlists MUST NOT contain a byte order mark (BOM)
*/
public Builder useByteOrderMark() {
mUseByteOrderMark = true;
return this;
}
public PlaylistWriter build() {
return new PlaylistWriter(mOutputStream, mFormat, mEncoding, mUseByteOrderMark);
}
}
}

View File

@ -0,0 +1,9 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.Playlist;
import java.io.IOException;
interface SectionWriter {
void write(TagWriter tagWriter, Playlist playlist) throws IOException, ParseException;
}

View File

@ -0,0 +1,33 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.OutputStreamWriter;
class TagWriter {
private final OutputStreamWriter mWriter;
public TagWriter(OutputStreamWriter outputStreamWriter) {
mWriter = outputStreamWriter;
}
public void write(String str) throws IOException {
mWriter.write(str);
}
public void writeLine(String line) throws IOException {
write(line + Constants.WRITE_NEW_LINE);
}
public void writeTag(String tag) throws IOException {
writeLine(Constants.COMMENT_PREFIX + tag);
}
public void writeTag(String tag, String value) throws IOException {
writeLine(Constants.COMMENT_PREFIX + tag + Constants.EXT_TAG_END + value);
}
public void flush() throws IOException {
mWriter.flush();
}
}

View File

@ -0,0 +1,31 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.TrackData;
class TrackLineParser implements LineParser {
@Override
public void parse(String line, ParseState state) throws ParseException {
final TrackData.Builder builder = new TrackData.Builder();
final MediaParseState mediaState = state.getMedia();
if (state.isExtended() && mediaState.trackInfo == null) {
throw ParseException.create(ParseExceptionType.MISSING_TRACK_INFO, line);
}
mediaState.tracks.add(builder
.withUri(line)
.withTrackInfo(mediaState.trackInfo)
.withEncryptionData(mediaState.encryptionData)
.withProgramDateTime(mediaState.programDateTime)
.withDiscontinuity(mediaState.hasDiscontinuity)
.withMapInfo(mediaState.mapInfo)
.withByteRange(mediaState.byteRange)
.build());
mediaState.trackInfo = null;
mediaState.programDateTime = null;
mediaState.hasDiscontinuity = false;
mediaState.mapInfo = null;
mediaState.byteRange = null;
}
}

View File

@ -0,0 +1,89 @@
package com.iheartradio.m3u8;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import com.iheartradio.m3u8.data.Resolution;
public class WriteUtil {
public static String writeYesNo(boolean yes) {
if (yes) {
return Constants.YES;
} else {
return Constants.NO;
}
}
public static String writeHexadecimal(List<Byte> hex) {
if (hex == null || hex.size() == 0) {
throw new IllegalArgumentException("hex might not be null or empty!");
}
final String prefix = "0x";
StringBuilder builder = new StringBuilder(hex.size() + prefix.length());
builder.append(prefix);
for(Byte b : hex) {
builder.append(String.format("%02x", b));
}
return builder.toString();
}
public static String writeResolution(Resolution r) {
return r.width + "x" + r.height;
}
public static String writeQuotedString(String unquotedString, String tag) throws ParseException {
return writeQuotedString(unquotedString, tag, false);
}
public static String writeQuotedString(String unquotedString, String tag, boolean optional) throws ParseException {
if (unquotedString != null || !optional) {
final StringBuilder builder = new StringBuilder(unquotedString.length() + 2);
builder.append("\"");
for (int i = 0; i < unquotedString.length(); ++i) {
final char c = unquotedString.charAt(i);
if (i == 0 && ParseUtil.isWhitespace(c)) {
throw new ParseException(ParseExceptionType.ILLEGAL_WHITESPACE, tag);
} else if (c == '"') {
builder.append('\\').append(c);
} else {
builder.append(c);
}
}
builder.append("\"");
return builder.toString();
}
return "\"\"";
}
public static String encodeUri(String decodedUri) throws ParseException {
try {
return URLEncoder.encode(decodedUri.replace("%2B", "+"), "utf-8");
} catch (UnsupportedEncodingException exception) {
throw new ParseException(ParseExceptionType.INTERNAL_ERROR);
}
}
public static String join(List<? extends Object> valueList, String separator) {
if (valueList == null || valueList.size() == 0) {
throw new IllegalArgumentException("valueList might not be null or empty!");
}
if (separator == null) {
throw new IllegalArgumentException("separator might not be null!");
}
StringBuilder sb = new StringBuilder();
for(int i = 0; i < valueList.size(); i++) {
sb.append(valueList.get(i).toString());
if (i + 1 < valueList.size()) {
sb.append(separator);
}
}
return sb.toString();
}
}

View File

@ -0,0 +1,42 @@
package com.iheartradio.m3u8;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import com.iheartradio.m3u8.data.Playlist;
abstract class Writer {
final TagWriter tagWriter;
Writer(OutputStream outputStream, Encoding encoding) {
try {
tagWriter = new TagWriter(new OutputStreamWriter(outputStream, encoding.getValue()));
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
void writeTagLine(String tag) throws IOException {
writeLine(Constants.COMMENT_PREFIX + tag);
}
void writeTagLine(String tag, Object value) throws IOException {
writeLine(Constants.COMMENT_PREFIX + tag + Constants.EXT_TAG_END + value);
}
void writeLine(String line) throws IOException {
tagWriter.write(line);
tagWriter.write("\n");
}
final void write(Playlist playlist) throws IOException, ParseException, PlaylistException {
doWrite(playlist);
tagWriter.flush();
}
abstract void doWrite(Playlist playlist) throws IOException, ParseException, PlaylistException;
}

View File

@ -0,0 +1,56 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class ByteRange {
private final long mSubRangeLength;
private final Long mOffset;
public ByteRange(long subRangeLength, long offset) {
this.mSubRangeLength = subRangeLength;
this.mOffset = offset;
}
public ByteRange(long subRangeLength, Long offset) {
this.mSubRangeLength = subRangeLength;
this.mOffset = offset;
}
public ByteRange(long subRangeLength) {
this(subRangeLength, null);
}
public long getSubRangeLength() {
return mSubRangeLength;
}
public Long getOffset() {
return mOffset;
}
public boolean hasOffset() {
return mOffset != null;
}
@Override
public String toString() {
return "ByteRange{" +
"mSubRangeLength=" + mSubRangeLength +
", mOffset=" + mOffset +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ByteRange byteRange = (ByteRange) o;
return mSubRangeLength == byteRange.mSubRangeLength &&
Objects.equals(mOffset, byteRange.mOffset);
}
@Override
public int hashCode() {
return Objects.hash(mSubRangeLength, mOffset);
}
}

View File

@ -0,0 +1,10 @@
package com.iheartradio.m3u8.data;
import java.util.Collections;
import java.util.List;
class DataUtil {
static <T> List<T> emptyOrUnmodifiable(final List<T> list) {
return list == null ? Collections.<T>emptyList() : Collections.unmodifiableList(list);
}
}

View File

@ -0,0 +1,129 @@
package com.iheartradio.m3u8.data;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class EncryptionData {
private final EncryptionMethod mMethod;
private final String mUri;
private final List<Byte> mInitializationVector;
private final String mKeyFormat;
private final List<Integer> mKeyFormatVersions;
private EncryptionData(EncryptionMethod method, String uri, List<Byte> initializationVector, String keyFormat, List<Integer> keyFormats) {
mMethod = method;
mUri = uri;
mInitializationVector = initializationVector == null ? null : Collections.unmodifiableList(initializationVector);
mKeyFormat = keyFormat;
mKeyFormatVersions = keyFormats == null ? null : Collections.unmodifiableList(keyFormats);
}
public EncryptionMethod getMethod() {
return mMethod;
}
public boolean hasUri() {
return mUri != null && !mUri.isEmpty();
}
public String getUri() {
return mUri;
}
public boolean hasInitializationVector() {
return mInitializationVector != null;
}
public List<Byte> getInitializationVector() {
return mInitializationVector;
}
public boolean hasKeyFormat() {
return mKeyFormat != null;
}
public String getKeyFormat() {
return mKeyFormat;
}
public boolean hasKeyFormatVersions() {
return mKeyFormatVersions != null;
}
public List<Integer> getKeyFormatVersions() {
return mKeyFormatVersions;
}
public Builder buildUpon() {
return new Builder(mMethod, mUri, mInitializationVector, mKeyFormat, mKeyFormatVersions);
}
@Override
public int hashCode() {
return Objects.hash(mInitializationVector, mKeyFormat, mKeyFormatVersions, mMethod, mUri);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof EncryptionData)) {
return false;
}
EncryptionData other = (EncryptionData) o;
return Objects.equals(this.mInitializationVector, other.mInitializationVector) &&
Objects.equals(this.mKeyFormat, other.mKeyFormat) &&
Objects.equals(this.mKeyFormatVersions, other.mKeyFormatVersions) &&
Objects.equals(this.mMethod, other.mMethod) &&
Objects.equals(this.mUri, other.mUri);
}
public static class Builder {
private EncryptionMethod mMethod;
private String mUri;
private List<Byte> mInitializationVector;
private String mKeyFormat;
private List<Integer> mKeyFormatVersions;
public Builder() {
}
private Builder(EncryptionMethod method, String uri, List<Byte> initializationVector, String keyFormat, List<Integer> keyFormatVersions) {
mMethod = method;
mUri = uri;
mInitializationVector = initializationVector;
mKeyFormat = keyFormat;
mKeyFormatVersions = keyFormatVersions;
}
public Builder withMethod(EncryptionMethod method) {
mMethod = method;
return this;
}
public Builder withUri(String uri) {
mUri = uri;
return this;
}
public Builder withInitializationVector(List<Byte> initializationVector) {
mInitializationVector = initializationVector;
return this;
}
public Builder withKeyFormat(String keyFormat) {
mKeyFormat = keyFormat;
return this;
}
public Builder withKeyFormatVersions(List<Integer> keyFormatVersions) {
mKeyFormatVersions = keyFormatVersions;
return this;
}
public EncryptionData build() {
return new EncryptionData(mMethod, mUri, mInitializationVector, mKeyFormat, mKeyFormatVersions);
}
}
}

View File

@ -0,0 +1,32 @@
package com.iheartradio.m3u8.data;
import java.util.HashMap;
import java.util.Map;
public enum EncryptionMethod {
NONE("NONE"),
AES("AES-128"),
SAMPLE_AES("SAMPLE-AES");
private static final Map<String, EncryptionMethod> sMap = new HashMap<String, EncryptionMethod>();
private final String value;
static {
for (EncryptionMethod encryptionMethod : EncryptionMethod.values()) {
sMap.put(encryptionMethod.value, encryptionMethod);
}
}
private EncryptionMethod(String value) {
this.value = value;
}
public static EncryptionMethod fromValue(String value) {
return sMap.get(value);
}
public String getValue() {
return this.value;
}
}

View File

@ -0,0 +1,214 @@
package com.iheartradio.m3u8.data;
import java.util.List;
import java.util.Objects;
public class IFrameStreamInfo implements IStreamInfo {
public static final int NO_BANDWIDTH = -1;
private final int mBandwidth;
private final int mAverageBandwidth;
private final List<String> mCodecs;
private final Resolution mResolution;
private final float mFrameRate;
private final String mVideo;
private final String mUri;
private IFrameStreamInfo(
int bandwidth,
int averageBandwidth,
List<String> codecs,
Resolution resolution,
float frameRate,
String video,
String uri) {
mBandwidth = bandwidth;
mAverageBandwidth = averageBandwidth;
mCodecs = codecs;
mResolution = resolution;
mFrameRate = frameRate;
mVideo = video;
mUri = uri;
}
@Override
public int getBandwidth() {
return mBandwidth;
}
@Override
public boolean hasAverageBandwidth() {
return mAverageBandwidth != NO_BANDWIDTH;
}
@Override
public int getAverageBandwidth() {
return mAverageBandwidth;
}
@Override
public boolean hasCodecs() {
return mCodecs != null;
}
@Override
public List<String> getCodecs() {
return mCodecs;
}
@Override
public boolean hasResolution() {
return mResolution != null;
}
@Override
public Resolution getResolution() {
return mResolution;
}
@Override
public boolean hasFrameRate() {
return !Float.isNaN(mFrameRate);
}
@Override
public float getFrameRate() {
return mFrameRate;
}
@Override
public boolean hasVideo() {
return mVideo != null;
}
@Override
public String getVideo() {
return mVideo;
}
public String getUri() {
return mUri;
}
public Builder buildUpon() {
return new Builder(
mBandwidth,
mAverageBandwidth,
mCodecs,
mResolution,
mFrameRate,
mVideo,
mUri);
}
@Override
public int hashCode() {
return Objects.hash(
mBandwidth,
mAverageBandwidth,
mCodecs,
mResolution,
mFrameRate,
mVideo,
mUri);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof IFrameStreamInfo)) {
return false;
}
IFrameStreamInfo other = (IFrameStreamInfo) o;
return mBandwidth == other.mBandwidth &&
mAverageBandwidth == other.mAverageBandwidth &&
Objects.equals(mCodecs, other.mCodecs) &&
Objects.equals(mResolution, other.mResolution) &&
Objects.equals(mFrameRate, other.mFrameRate) &&
Objects.equals(mVideo, other.mVideo) &&
Objects.equals(mUri, other.mUri);
}
public static class Builder implements StreamInfoBuilder {
private int mBandwidth = NO_BANDWIDTH;
private int mAverageBandwidth = NO_BANDWIDTH;
private List<String> mCodecs;
private Resolution mResolution;
private float mFrameRate = Float.NaN;
private String mVideo;
private String mUri;
public Builder() {
}
private Builder(
int bandwidth,
int averageBandwidth,
List<String> codecs,
Resolution resolution,
float frameRate,
String video,
String uri) {
mBandwidth = bandwidth;
mAverageBandwidth = averageBandwidth;
mCodecs = codecs;
mResolution = resolution;
mFrameRate = frameRate;
mVideo = video;
mUri = uri;
}
@Override
public Builder withBandwidth(int bandwidth) {
mBandwidth = bandwidth;
return this;
}
@Override
public Builder withAverageBandwidth(int averageBandwidth) {
mAverageBandwidth = averageBandwidth;
return this;
}
@Override
public Builder withCodecs(List<String> codecs) {
mCodecs = codecs;
return this;
}
@Override
public Builder withResolution(Resolution resolution) {
mResolution = resolution;
return this;
}
@Override
public Builder withFrameRate(float frameRate) {
mFrameRate = frameRate;
return this;
}
@Override
public Builder withVideo(String video) {
mVideo = video;
return this;
}
public Builder withUri(String uri) {
mUri = uri;
return this;
}
public IFrameStreamInfo build() {
return new IFrameStreamInfo(
mBandwidth,
mAverageBandwidth,
mCodecs,
mResolution,
mFrameRate,
mVideo,
mUri);
}
}
}

View File

@ -0,0 +1,27 @@
package com.iheartradio.m3u8.data;
import java.util.List;
public interface IStreamInfo {
public int getBandwidth();
boolean hasAverageBandwidth();
int getAverageBandwidth();
boolean hasCodecs();
List<String> getCodecs();
boolean hasResolution();
Resolution getResolution();
boolean hasFrameRate();
float getFrameRate();
boolean hasVideo();
String getVideo();
}

View File

@ -0,0 +1,78 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class MapInfo {
private final String uri;
private final ByteRange byteRange;
public MapInfo(String uri, ByteRange byteRange) {
this.uri = uri;
this.byteRange = byteRange;
}
public String getUri() {
return uri;
}
public boolean hasByteRange() {
return byteRange != null;
}
public ByteRange getByteRange() {
return byteRange;
}
public Builder buildUpon() {
return new Builder(uri, byteRange);
}
@Override
public String toString() {
return "MapInfo{" +
"uri='" + uri + '\'' +
", byteRange='" + byteRange + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MapInfo mapInfo = (MapInfo) o;
return Objects.equals(uri, mapInfo.uri) &&
Objects.equals(byteRange, mapInfo.byteRange);
}
@Override
public int hashCode() {
return Objects.hash(uri, byteRange);
}
public static class Builder {
private String mUri;
private ByteRange mByteRange;
public Builder() {
}
private Builder(String uri, ByteRange byteRange) {
this.mUri = uri;
this.mByteRange = byteRange;
}
public Builder withUri(String uri) {
this.mUri = uri;
return this;
}
public Builder withByteRange(ByteRange byteRange) {
this.mByteRange = byteRange;
return this;
}
public MapInfo build() {
return new MapInfo(mUri, mByteRange);
}
}
}

View File

@ -0,0 +1,137 @@
package com.iheartradio.m3u8.data;
import java.util.List;
import java.util.Objects;
public class MasterPlaylist {
private final List<PlaylistData> mPlaylists;
private final List<IFrameStreamInfo> mIFramePlaylists;
private final List<MediaData> mMediaData;
private final List<String> mUnknownTags;
private final StartData mStartData;
private MasterPlaylist(List<PlaylistData> playlists, List<IFrameStreamInfo> iFramePlaylists, List<MediaData> mediaData, List<String> unknownTags, StartData startData) {
mPlaylists = DataUtil.emptyOrUnmodifiable(playlists);
mIFramePlaylists = DataUtil.emptyOrUnmodifiable(iFramePlaylists);
mMediaData = DataUtil.emptyOrUnmodifiable(mediaData);
mUnknownTags = DataUtil.emptyOrUnmodifiable(unknownTags);
mStartData = startData;
}
public List<PlaylistData> getPlaylists() {
return mPlaylists;
}
public List<IFrameStreamInfo> getIFramePlaylists() {
return mIFramePlaylists;
}
public List<MediaData> getMediaData() {
return mMediaData;
}
public boolean hasUnknownTags() {
return mUnknownTags.size() > 0;
}
public List<String> getUnknownTags() {
return mUnknownTags;
}
public boolean hasStartData() {
return mStartData != null;
}
public StartData getStartData() {
return mStartData;
}
public Builder buildUpon() {
return new Builder(mPlaylists, mIFramePlaylists, mMediaData, mUnknownTags);
}
@Override
public int hashCode() {
return Objects.hash(mMediaData, mPlaylists, mIFramePlaylists, mUnknownTags, mStartData);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof MasterPlaylist)) {
return false;
}
MasterPlaylist other = (MasterPlaylist) o;
return Objects.equals(mMediaData, other.mMediaData) &&
Objects.equals(mPlaylists, other.mPlaylists) &&
Objects.equals(mIFramePlaylists, other.mIFramePlaylists) &&
Objects.equals(mUnknownTags, other.mUnknownTags) &&
Objects.equals(mStartData, other.mStartData);
}
@Override
public String toString() {
return new StringBuilder()
.append("(MasterPlaylist")
.append(" mPlaylists=").append(mPlaylists.toString())
.append(" mIFramePlaylists=").append(mIFramePlaylists.toString())
.append(" mMediaData=").append(mMediaData.toString())
.append(" mUnknownTags=").append(mUnknownTags.toString())
.append(" mStartData=").append(mStartData.toString())
.append(")")
.toString();
}
public static class Builder {
private List<PlaylistData> mPlaylists;
private List<IFrameStreamInfo> mIFramePlaylists;
private List<MediaData> mMediaData;
private List<String> mUnknownTags;
private StartData mStartData;
public Builder() {
}
private Builder(List<PlaylistData> playlists, List<IFrameStreamInfo> iFramePlaylists, List<MediaData> mediaData, List<String> unknownTags) {
mPlaylists = playlists;
mIFramePlaylists = iFramePlaylists;
mMediaData = mediaData;
mUnknownTags = unknownTags;
}
private Builder(List<PlaylistData> playlists, List<MediaData> mediaData) {
mPlaylists = playlists;
mMediaData = mediaData;
}
public Builder withPlaylists(List<PlaylistData> playlists) {
mPlaylists = playlists;
return this;
}
public Builder withIFramePlaylists(List<IFrameStreamInfo> iFramePlaylists) {
mIFramePlaylists = iFramePlaylists;
return this;
}
public Builder withMediaData(List<MediaData> mediaData) {
mMediaData = mediaData;
return this;
}
public Builder withUnknownTags(List<String> unknownTags) {
mUnknownTags = unknownTags;
return this;
}
public Builder withStartData(StartData startData) {
mStartData = startData;
return this;
}
public MasterPlaylist build() {
return new MasterPlaylist(mPlaylists, mIFramePlaylists, mMediaData, mUnknownTags, mStartData);
}
}
}

View File

@ -0,0 +1,307 @@
package com.iheartradio.m3u8.data;
import java.util.List;
import java.util.Objects;
public class MediaData {
public static final int NO_CHANNELS = -1;
private final MediaType mType;
private final String mUri;
private final String mGroupId;
private final String mLanguage;
private final String mAssociatedLanguage;
private final String mName;
private final boolean mDefault;
private final boolean mAutoSelect;
private final boolean mForced;
private final String mInStreamId;
private final List<String> mCharacteristics;
private final int mChannels;
private MediaData(
MediaType type,
String uri,
String groupId,
String language,
String associatedLanguage,
String name,
boolean isDefault,
boolean isAutoSelect,
boolean isForced,
String inStreamId,
List<String> characteristics,
int channels) {
mType = type;
mUri = uri;
mGroupId = groupId;
mLanguage = language;
mAssociatedLanguage = associatedLanguage;
mName = name;
mDefault = isDefault;
mAutoSelect = isAutoSelect;
mForced = isForced;
mInStreamId = inStreamId;
mCharacteristics = DataUtil.emptyOrUnmodifiable(characteristics);
mChannels = channels;
}
public MediaType getType() {
return mType;
}
public boolean hasUri() {
return mUri != null && !mUri.isEmpty();
}
public String getUri() {
return mUri;
}
public String getGroupId() {
return mGroupId;
}
public boolean hasLanguage() {
return mLanguage != null;
}
public String getLanguage() {
return mLanguage;
}
public boolean hasAssociatedLanguage() {
return mAssociatedLanguage != null;
}
public String getAssociatedLanguage() {
return mAssociatedLanguage;
}
public String getName() {
return mName;
}
public boolean isDefault() {
return mDefault;
}
public boolean isAutoSelect() {
return mAutoSelect;
}
public boolean isForced() {
return mForced;
}
public boolean hasInStreamId() {
return mInStreamId != null;
}
public String getInStreamId() {
return mInStreamId;
}
public boolean hasCharacteristics() {
return !mCharacteristics.isEmpty();
}
public List<String> getCharacteristics() {
return mCharacteristics;
}
public boolean hasChannels() {
return mChannels != NO_CHANNELS;
}
public Integer getChannels() {
return mChannels;
}
public Builder buildUpon() {
return new Builder(
mType,
mUri,
mGroupId,
mLanguage,
mAssociatedLanguage,
mName,
mDefault,
mAutoSelect,
mForced,
mInStreamId,
mCharacteristics,
mChannels);
}
@Override
public int hashCode() {
return Objects.hash(
mAssociatedLanguage,
mAutoSelect,
mCharacteristics,
mDefault,
mForced,
mGroupId,
mInStreamId,
mLanguage,
mName,
mType,
mUri,
mChannels);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof MediaData)) {
return false;
}
MediaData other = (MediaData) o;
return mType == other.mType &&
Objects.equals(mUri, other.mUri) &&
Objects.equals(mGroupId, other.mGroupId) &&
Objects.equals(mLanguage, other.mLanguage) &&
Objects.equals(mAssociatedLanguage, other.mAssociatedLanguage) &&
Objects.equals(mName, other.mName) &&
mDefault == other.mDefault &&
mAutoSelect == other.mAutoSelect &&
mForced == other.mForced &&
Objects.equals(mInStreamId, other.mInStreamId) &&
Objects.equals(mCharacteristics, other.mCharacteristics) &&
mChannels == other.mChannels;
}
public static class Builder {
private MediaType mType;
private String mUri;
private String mGroupId;
private String mLanguage;
private String mAssociatedLanguage;
private String mName;
private boolean mDefault;
private boolean mAutoSelect;
private boolean mForced;
private String mInStreamId;
private List<String> mCharacteristics;
private int mChannels = NO_CHANNELS;
public Builder() {
}
private Builder(
MediaType type,
String uri,
String groupId,
String language,
String associatedLanguage,
String name,
boolean isDefault,
boolean autoSelect,
boolean forced,
String inStreamId,
List<String> characteristics,
int channels) {
mType = type;
mUri = uri;
mGroupId = groupId;
mLanguage = language;
mAssociatedLanguage = associatedLanguage;
mName = name;
mDefault = isDefault;
mAutoSelect = autoSelect;
mForced = forced;
mInStreamId = inStreamId;
mCharacteristics = characteristics;
mChannels = channels;
}
public Builder withType(MediaType type) {
mType = type;
return this;
}
public Builder withUri(String uri) {
mUri = uri;
return this;
}
public Builder withGroupId(String groupId) {
mGroupId = groupId;
return this;
}
public Builder withLanguage(String language) {
mLanguage = language;
return this;
}
public Builder withAssociatedLanguage(String associatedLanguage) {
mAssociatedLanguage = associatedLanguage;
return this;
}
public Builder withName(String name) {
mName = name;
return this;
}
public Builder withDefault(boolean isDefault) {
mDefault = isDefault;
return this;
}
public Builder withAutoSelect(boolean isAutoSelect) {
mAutoSelect = isAutoSelect;
return this;
}
public Builder withForced(boolean isForced) {
mForced = isForced;
return this;
}
public Builder withInStreamId(String inStreamId) {
mInStreamId = inStreamId;
return this;
}
public Builder withCharacteristics(List<String> characteristics) {
mCharacteristics = characteristics;
return this;
}
public Builder withChannels(int channels) {
mChannels = channels;
return this;
}
public MediaData build() {
return new MediaData(
mType,
mUri,
mGroupId,
mLanguage,
mAssociatedLanguage,
mName,
mDefault,
mAutoSelect,
mForced,
mInStreamId,
mCharacteristics,
mChannels);
}
}
@Override
public String toString() {
return "MediaData [mType=" + mType + ", mUri=" + mUri + ", mGroupId="
+ mGroupId + ", mLanguage=" + mLanguage
+ ", mAssociatedLanguage=" + mAssociatedLanguage + ", mName="
+ mName + ", mDefault=" + mDefault + ", mAutoSelect="
+ mAutoSelect + ", mForced=" + mForced + ", mInStreamId="
+ mInStreamId + ", mCharacteristics=" + mCharacteristics
+ ", mChannels=" + mChannels + "]";
}
}

View File

@ -0,0 +1,210 @@
package com.iheartradio.m3u8.data;
import java.util.List;
import java.util.Objects;
public class MediaPlaylist {
private final List<TrackData> mTracks;
private final List<String> mUnknownTags;
private final int mTargetDuration;
private final int mMediaSequenceNumber;
private final boolean mIsIframesOnly;
private final boolean mIsOngoing;
private final PlaylistType mPlaylistType;
private final StartData mStartData;
private MediaPlaylist(List<TrackData> tracks, List<String> unknownTags, int targetDuration, StartData startData, int mediaSequenceNumber, boolean isIframesOnly, boolean isOngoing, PlaylistType playlistType) {
mTracks = DataUtil.emptyOrUnmodifiable(tracks);
mUnknownTags = DataUtil.emptyOrUnmodifiable(unknownTags);
mTargetDuration = targetDuration;
mMediaSequenceNumber = mediaSequenceNumber;
mIsIframesOnly = isIframesOnly;
mIsOngoing = isOngoing;
mStartData = startData;
mPlaylistType = playlistType;
}
public boolean hasTracks() {
return !mTracks.isEmpty();
}
public List<TrackData> getTracks() {
return mTracks;
}
public int getTargetDuration() {
return mTargetDuration;
}
public int getMediaSequenceNumber() {
return mMediaSequenceNumber;
}
public boolean isIframesOnly() {
return mIsIframesOnly;
}
public boolean isOngoing() {
return mIsOngoing;
}
public boolean hasUnknownTags() {
return !mUnknownTags.isEmpty();
}
public List<String> getUnknownTags() {
return mUnknownTags;
}
public StartData getStartData() {
return mStartData;
}
public boolean hasStartData() {
return mStartData != null;
}
public PlaylistType getPlaylistType() {
return mPlaylistType;
}
public boolean hasPlaylistType() {
return mPlaylistType != null;
}
public int getDiscontinuitySequenceNumber(final int segmentIndex) {
if (segmentIndex < 0 || segmentIndex >= mTracks.size()) {
throw new IndexOutOfBoundsException();
}
int discontinuitySequenceNumber = 0;
for (int i = 0; i <= segmentIndex; ++i) {
if (mTracks.get(i).hasDiscontinuity()) {
++discontinuitySequenceNumber;
}
}
return discontinuitySequenceNumber;
}
public Builder buildUpon() {
return new Builder(mTracks, mUnknownTags, mTargetDuration, mMediaSequenceNumber, mIsIframesOnly, mIsOngoing, mPlaylistType, mStartData);
}
@Override
public int hashCode() {
return Objects.hash(
mTracks,
mUnknownTags,
mTargetDuration,
mMediaSequenceNumber,
mIsIframesOnly,
mIsOngoing,
mPlaylistType,
mStartData);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof MediaPlaylist)) {
return false;
}
MediaPlaylist other = (MediaPlaylist) o;
return Objects.equals(mTracks, other.mTracks) &&
Objects.equals(mUnknownTags, other.mUnknownTags) &&
mTargetDuration == other.mTargetDuration &&
mMediaSequenceNumber == other.mMediaSequenceNumber &&
mIsIframesOnly == other.mIsIframesOnly &&
mIsOngoing == other.mIsOngoing &&
Objects.equals(mPlaylistType, other.mPlaylistType) &&
Objects.equals(mStartData, other.mStartData);
}
@Override
public String toString() {
return new StringBuilder()
.append("(MediaPlaylist")
.append(" mTracks=").append(mTracks)
.append(" mUnknownTags=").append(mUnknownTags)
.append(" mTargetDuration=").append(mTargetDuration)
.append(" mMediaSequenceNumber=").append(mMediaSequenceNumber)
.append(" mIsIframesOnly=").append(mIsIframesOnly)
.append(" mIsOngoing=").append(mIsOngoing)
.append(" mPlaylistType=").append(mPlaylistType)
.append(" mStartData=").append(mStartData)
.append(")")
.toString();
}
public static class Builder {
private List<TrackData> mTracks;
private List<String> mUnknownTags;
private int mTargetDuration;
private int mMediaSequenceNumber;
private boolean mIsIframesOnly;
private boolean mIsOngoing;
private PlaylistType mPlaylistType;
private StartData mStartData;
public Builder() {
}
private Builder(List<TrackData> tracks, List<String> unknownTags, int targetDuration, int mediaSequenceNumber, boolean isIframesOnly, boolean isOngoing, PlaylistType playlistType, StartData startData) {
mTracks = tracks;
mUnknownTags = unknownTags;
mTargetDuration = targetDuration;
mMediaSequenceNumber = mediaSequenceNumber;
mIsIframesOnly = isIframesOnly;
mIsOngoing = isOngoing;
mPlaylistType = playlistType;
mStartData = startData;
}
public Builder withTracks(List<TrackData> tracks) {
mTracks = tracks;
return this;
}
public Builder withUnknownTags(List<String> unknownTags) {
mUnknownTags = unknownTags;
return this;
}
public Builder withTargetDuration(int targetDuration) {
mTargetDuration = targetDuration;
return this;
}
public Builder withStartData(StartData startData) {
mStartData = startData;
return this;
}
public Builder withMediaSequenceNumber(int mediaSequenceNumber) {
mMediaSequenceNumber = mediaSequenceNumber;
return this;
}
public Builder withIsIframesOnly(boolean isIframesOnly) {
mIsIframesOnly = isIframesOnly;
return this;
}
public Builder withIsOngoing(boolean isOngoing) {
mIsOngoing = isOngoing;
return this;
}
public Builder withPlaylistType(PlaylistType playlistType) {
mPlaylistType = playlistType;
return this;
}
public MediaPlaylist build() {
return new MediaPlaylist(mTracks, mUnknownTags, mTargetDuration, mStartData, mMediaSequenceNumber, mIsIframesOnly, mIsOngoing, mPlaylistType);
}
}
}

View File

@ -0,0 +1,33 @@
package com.iheartradio.m3u8.data;
import java.util.HashMap;
import java.util.Map;
public enum MediaType {
AUDIO("AUDIO"),
VIDEO("VIDEO"),
SUBTITLES("SUBTITLES"),
CLOSED_CAPTIONS("CLOSED-CAPTIONS");
private static final Map<String, MediaType> sMap = new HashMap<String, MediaType>();
private final String value;
static {
for (MediaType mediaType : MediaType.values()) {
sMap.put(mediaType.value, mediaType);
}
}
private MediaType(String value) {
this.value = value;
}
public static MediaType fromValue(String value) {
return sMap.get(value);
}
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,119 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class Playlist {
public static final int MIN_COMPATIBILITY_VERSION = 1;
private final MasterPlaylist mMasterPlaylist;
private final MediaPlaylist mMediaPlaylist;
private final boolean mIsExtended;
private final int mCompatibilityVersion;
private Playlist(MasterPlaylist masterPlaylist, MediaPlaylist mediaPlaylist, boolean isExtended, int compatibilityVersion) {
mMasterPlaylist = masterPlaylist;
mMediaPlaylist = mediaPlaylist;
mIsExtended = isExtended;
mCompatibilityVersion = compatibilityVersion;
}
public boolean hasMasterPlaylist() {
return mMasterPlaylist != null;
}
public boolean hasMediaPlaylist() {
return mMediaPlaylist != null;
}
public MasterPlaylist getMasterPlaylist() {
return mMasterPlaylist;
}
public MediaPlaylist getMediaPlaylist() {
return mMediaPlaylist;
}
public boolean isExtended() {
return mIsExtended;
}
public int getCompatibilityVersion() {
return mCompatibilityVersion;
}
public Builder buildUpon() {
return new Builder(mMasterPlaylist, mMediaPlaylist, mIsExtended, mCompatibilityVersion);
}
@Override
public int hashCode() {
return Objects.hash(mCompatibilityVersion, mIsExtended, mMasterPlaylist, mMediaPlaylist);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Playlist)) {
return false;
}
Playlist other = (Playlist) o;
return Objects.equals(mMasterPlaylist, other.mMasterPlaylist) &&
Objects.equals(mMediaPlaylist, other.mMediaPlaylist) &&
mIsExtended == other.mIsExtended &&
mCompatibilityVersion == other.mCompatibilityVersion;
}
@Override
public String toString() {
return new StringBuilder()
.append("(Playlist")
.append(" mMasterPlaylist=").append(mMasterPlaylist)
.append(" mMediaPlaylist=").append(mMediaPlaylist)
.append(" mIsExtended=").append(mIsExtended)
.append(" mCompatibilityVersion=").append(mCompatibilityVersion)
.append(")")
.toString();
}
public static class Builder {
private MasterPlaylist mMasterPlaylist;
private MediaPlaylist mMediaPlaylist;
private boolean mIsExtended;
private int mCompatibilityVersion = MIN_COMPATIBILITY_VERSION;
public Builder() {
}
private Builder(MasterPlaylist masterPlaylist, MediaPlaylist mediaPlaylist, boolean isExtended, int compatibilityVersion) {
mMasterPlaylist = masterPlaylist;
mMediaPlaylist = mediaPlaylist;
mIsExtended = isExtended;
mCompatibilityVersion = compatibilityVersion;
}
public Builder withMasterPlaylist(MasterPlaylist masterPlaylist) {
mMasterPlaylist = masterPlaylist;
return withExtended(true);
}
public Builder withMediaPlaylist(MediaPlaylist mediaPlaylist) {
mMediaPlaylist = mediaPlaylist;
return this;
}
public Builder withExtended(boolean isExtended) {
mIsExtended = isExtended;
return this;
}
public Builder withCompatibilityVersion(int version) {
mCompatibilityVersion = version;
return this;
}
public Playlist build() {
return new Playlist(mMasterPlaylist, mMediaPlaylist, mIsExtended, mCompatibilityVersion);
}
}
}

View File

@ -0,0 +1,82 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class PlaylistData {
private final String mUri;
private final StreamInfo mStreamInfo;
private PlaylistData(String uri, StreamInfo streamInfo) {
mUri = uri;
mStreamInfo = streamInfo;
}
public String getUri() {
return mUri;
}
public boolean hasStreamInfo() {
return mStreamInfo != null;
}
public StreamInfo getStreamInfo() {
return mStreamInfo;
}
public Builder buildUpon() {
return new Builder(mUri, mStreamInfo);
}
@Override
public int hashCode() {
return Objects.hash(mUri, mStreamInfo);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof PlaylistData)) {
return false;
}
PlaylistData other = (PlaylistData) o;
return Objects.equals(mUri, other.mUri) && Objects.equals(mStreamInfo, other.mStreamInfo);
}
@Override
public String toString() {
return "PlaylistData [mStreamInfo=" + mStreamInfo
+ ", mUri=" + mUri + "]";
}
public static class Builder {
private String mUri;
private StreamInfo mStreamInfo;
public Builder() {
}
private Builder(String uri, StreamInfo streamInfo) {
mUri = uri;
mStreamInfo = streamInfo;
}
public Builder withPath(String path) {
mUri = path;
return this;
}
public Builder withUri(String uri) {
mUri = uri;
return this;
}
public Builder withStreamInfo(StreamInfo streamInfo) {
mStreamInfo = streamInfo;
return this;
}
public PlaylistData build() {
return new PlaylistData(mUri, mStreamInfo);
}
}
}

View File

@ -0,0 +1,30 @@
package com.iheartradio.m3u8.data;
import java.util.HashMap;
import java.util.Map;
public enum PlaylistType {
EVENT("EVENT"), VOD("VOD");
private static final Map<String, PlaylistType> sMap = new HashMap<String, PlaylistType>();
private final String value;
static {
for (PlaylistType mediaType : PlaylistType.values()) {
sMap.put(mediaType.value, mediaType);
}
}
private PlaylistType(String value) {
this.value = value;
}
public static PlaylistType fromValue(String value) {
return sMap.get(value);
}
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,30 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class Resolution {
public final int width;
public final int height;
public Resolution(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int hashCode() {
return Objects.hash(height, width);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Resolution)) {
return false;
}
Resolution other = (Resolution) o;
return width == other.width &&
height == other.height;
}
}

View File

@ -0,0 +1,69 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class StartData {
private final float mTimeOffset;
private final boolean mPrecise;
public StartData(float timeOffset, boolean precise) {
mTimeOffset = timeOffset;
mPrecise = precise;
}
public float getTimeOffset() {
return mTimeOffset;
}
public boolean isPrecise() {
return mPrecise;
}
public Builder buildUpon() {
return new Builder(mTimeOffset, mPrecise);
}
@Override
public int hashCode() {
return Objects.hash(mPrecise, mTimeOffset);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof StartData)) {
return false;
}
StartData other = (StartData) o;
return this.mPrecise == other.mPrecise &&
this.mTimeOffset == other.mTimeOffset;
}
public static class Builder {
private float mTimeOffset = Float.NaN;
private boolean mPrecise;
public Builder() {
}
private Builder(float timeOffset, boolean precise) {
mTimeOffset = timeOffset;
mPrecise = precise;
}
public Builder withTimeOffset(float timeOffset) {
mTimeOffset = timeOffset;
return this;
}
public Builder withPrecise(boolean precise) {
mPrecise = precise;
return this;
}
public StartData build() {
return new StartData(mTimeOffset, mPrecise);
}
}
}

View File

@ -0,0 +1,264 @@
package com.iheartradio.m3u8.data;
import java.util.List;
import java.util.Objects;
public class StreamInfo implements IStreamInfo {
public static final int NO_BANDWIDTH = -1;
private final int mBandwidth;
private final int mAverageBandwidth;
private final List<String> mCodecs;
private final Resolution mResolution;
private final float mFrameRate;
private final String mAudio;
private final String mVideo;
private final String mSubtitles;
private final String mClosedCaptions;
private StreamInfo(
int bandwidth,
int averageBandwidth,
List<String> codecs,
Resolution resolution,
float frameRate,
String audio,
String video,
String subtitles,
String closedCaptions) {
mBandwidth = bandwidth;
mAverageBandwidth = averageBandwidth;
mCodecs = codecs;
mResolution = resolution;
mFrameRate = frameRate;
mAudio = audio;
mVideo = video;
mSubtitles = subtitles;
mClosedCaptions = closedCaptions;
}
@Override
public int getBandwidth() {
return mBandwidth;
}
@Override
public boolean hasAverageBandwidth() {
return mAverageBandwidth != NO_BANDWIDTH;
}
@Override
public int getAverageBandwidth() {
return mAverageBandwidth;
}
@Override
public boolean hasCodecs() {
return mCodecs != null;
}
@Override
public List<String> getCodecs() {
return mCodecs;
}
@Override
public boolean hasResolution() {
return mResolution != null;
}
@Override
public Resolution getResolution() {
return mResolution;
}
@Override
public boolean hasFrameRate() {
return !Float.isNaN(mFrameRate);
}
@Override
public float getFrameRate() {
return mFrameRate;
}
public boolean hasAudio() {
return mAudio != null;
}
public String getAudio() {
return mAudio;
}
@Override
public boolean hasVideo() {
return mVideo != null;
}
@Override
public String getVideo() {
return mVideo;
}
public boolean hasSubtitles() {
return mSubtitles != null;
}
public String getSubtitles() {
return mSubtitles;
}
public boolean hasClosedCaptions() {
return mClosedCaptions != null;
}
public String getClosedCaptions() {
return mClosedCaptions;
}
public Builder buildUpon() {
return new Builder(
mBandwidth,
mAverageBandwidth,
mCodecs,
mResolution,
mFrameRate,
mAudio,
mVideo,
mSubtitles,
mClosedCaptions);
}
@Override
public int hashCode() {
return Objects.hash(
mBandwidth,
mAverageBandwidth,
mCodecs,
mResolution,
mFrameRate,
mAudio,
mVideo,
mSubtitles,
mClosedCaptions);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof StreamInfo)) {
return false;
}
StreamInfo other = (StreamInfo) o;
return mBandwidth == other.mBandwidth &&
mAverageBandwidth == other.mAverageBandwidth &&
Objects.equals(mCodecs, other.mCodecs) &&
Objects.equals(mResolution, other.mResolution) &&
Objects.equals(mFrameRate, other.mFrameRate) &&
Objects.equals(mAudio, other.mAudio) &&
Objects.equals(mVideo, other.mVideo) &&
Objects.equals(mSubtitles, other.mSubtitles) &&
Objects.equals(mClosedCaptions, other.mClosedCaptions);
}
public static class Builder implements StreamInfoBuilder {
private int mBandwidth = NO_BANDWIDTH;
private int mAverageBandwidth = NO_BANDWIDTH;
private List<String> mCodecs;
private Resolution mResolution;
private float mFrameRate = Float.NaN;
private String mAudio;
private String mVideo;
private String mSubtitles;
private String mClosedCaptions;
public Builder() {
}
private Builder(
int bandwidth,
int averageBandwidth,
List<String> codecs,
Resolution resolution,
float frameRate,
String audio,
String video,
String subtitles,
String closedCaptions) {
mBandwidth = bandwidth;
mAverageBandwidth = averageBandwidth;
mCodecs = codecs;
mResolution = resolution;
mFrameRate = frameRate;
mAudio = audio;
mVideo = video;
mSubtitles = subtitles;
mClosedCaptions = closedCaptions;
}
@Override
public Builder withBandwidth(int bandwidth) {
mBandwidth = bandwidth;
return this;
}
@Override
public Builder withAverageBandwidth(int averageBandwidth) {
mAverageBandwidth = averageBandwidth;
return this;
}
@Override
public Builder withCodecs(List<String> codecs) {
mCodecs = codecs;
return this;
}
@Override
public Builder withResolution(Resolution resolution) {
mResolution = resolution;
return this;
}
@Override
public Builder withFrameRate(float frameRate) {
mFrameRate = frameRate;
return this;
}
public Builder withAudio(String audio) {
mAudio = audio;
return this;
}
@Override
public Builder withVideo(String video) {
mVideo = video;
return this;
}
public Builder withSubtitles(String subtitles) {
mSubtitles = subtitles;
return this;
}
public Builder withClosedCaptions(String closedCaptions) {
mClosedCaptions = closedCaptions;
return this;
}
public StreamInfo build() {
return new StreamInfo(
mBandwidth,
mAverageBandwidth,
mCodecs,
mResolution,
mFrameRate,
mAudio,
mVideo,
mSubtitles,
mClosedCaptions);
}
}
}

View File

@ -0,0 +1,17 @@
package com.iheartradio.m3u8.data;
import java.util.List;
public interface StreamInfoBuilder {
public StreamInfoBuilder withBandwidth(int bandwidth);
public StreamInfoBuilder withAverageBandwidth(int averageBandwidth);
public StreamInfoBuilder withCodecs(List<String> codecs);
public StreamInfoBuilder withResolution(Resolution resolution);
public StreamInfoBuilder withFrameRate(float frameRate);
public StreamInfoBuilder withVideo(String video);
}

View File

@ -0,0 +1,174 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class TrackData {
private final String mUri;
private final TrackInfo mTrackInfo;
private final EncryptionData mEncryptionData;
private final String mProgramDateTime;
private final boolean mHasDiscontinuity;
private final MapInfo mMapInfo;
private final ByteRange mByteRange;
private TrackData(String uri, TrackInfo trackInfo, EncryptionData encryptionData, String programDateTime, boolean hasDiscontinuity, MapInfo mapInfo, ByteRange byteRange) {
mUri = uri;
mTrackInfo = trackInfo;
mEncryptionData = encryptionData;
mProgramDateTime = programDateTime;
mHasDiscontinuity = hasDiscontinuity;
mMapInfo = mapInfo;
mByteRange = byteRange;
}
public String getUri() {
return mUri;
}
public boolean hasTrackInfo() {
return mTrackInfo != null;
}
public TrackInfo getTrackInfo() {
return mTrackInfo;
}
public boolean hasEncryptionData() {
return mEncryptionData != null;
}
public boolean isEncrypted() {
return hasEncryptionData() &&
mEncryptionData.getMethod() != null &&
mEncryptionData.getMethod() != EncryptionMethod.NONE;
}
public boolean hasProgramDateTime() {
return mProgramDateTime != null && mProgramDateTime.length() > 0;
}
public String getProgramDateTime() {
return mProgramDateTime;
}
public boolean hasDiscontinuity() {
return mHasDiscontinuity;
}
public EncryptionData getEncryptionData() {
return mEncryptionData;
}
public boolean hasMapInfo() {
return mMapInfo != null;
}
public MapInfo getMapInfo() {
return mMapInfo;
}
public boolean hasByteRange() {
return mByteRange != null;
}
public ByteRange getByteRange() {
return mByteRange;
}
public Builder buildUpon() {
return new Builder(getUri(), mTrackInfo, mEncryptionData, mHasDiscontinuity, mMapInfo, mByteRange);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TrackData trackData = (TrackData) o;
return mHasDiscontinuity == trackData.mHasDiscontinuity &&
Objects.equals(mUri, trackData.mUri) &&
Objects.equals(mTrackInfo, trackData.mTrackInfo) &&
Objects.equals(mEncryptionData, trackData.mEncryptionData) &&
Objects.equals(mProgramDateTime, trackData.mProgramDateTime) &&
Objects.equals(mMapInfo, trackData.mMapInfo) &&
Objects.equals(mByteRange, trackData.mByteRange);
}
@Override
public int hashCode() {
return Objects.hash(mUri, mTrackInfo, mEncryptionData, mProgramDateTime, mHasDiscontinuity, mMapInfo, mByteRange);
}
@Override
public String toString() {
return "TrackData{" +
"mUri='" + mUri + '\'' +
", mTrackInfo=" + mTrackInfo +
", mEncryptionData=" + mEncryptionData +
", mProgramDateTime='" + mProgramDateTime + '\'' +
", mHasDiscontinuity=" + mHasDiscontinuity +
", mMapInfo=" + mMapInfo +
", mByteRange=" + mByteRange +
'}';
}
public static class Builder {
private String mUri;
private TrackInfo mTrackInfo;
private EncryptionData mEncryptionData;
private String mProgramDateTime;
private boolean mHasDiscontinuity;
private MapInfo mMapInfo;
private ByteRange mByteRange;
public Builder() {
}
private Builder(String uri, TrackInfo trackInfo, EncryptionData encryptionData, boolean hasDiscontinuity, MapInfo mapInfo, ByteRange byteRange) {
mUri = uri;
mTrackInfo = trackInfo;
mEncryptionData = encryptionData;
mHasDiscontinuity = hasDiscontinuity;
mMapInfo = mapInfo;
mByteRange = byteRange;
}
public Builder withUri(String url) {
mUri = url;
return this;
}
public Builder withTrackInfo(TrackInfo trackInfo) {
mTrackInfo = trackInfo;
return this;
}
public Builder withEncryptionData(EncryptionData encryptionData) {
mEncryptionData = encryptionData;
return this;
}
public Builder withProgramDateTime(String programDateTime) {
mProgramDateTime = programDateTime;
return this;
}
public Builder withDiscontinuity(boolean hasDiscontinuity) {
mHasDiscontinuity = hasDiscontinuity;
return this;
}
public Builder withMapInfo(MapInfo mapInfo) {
mMapInfo = mapInfo;
return this;
}
public Builder withByteRange(ByteRange byteRange) {
mByteRange = byteRange;
return this;
}
public TrackData build() {
return new TrackData(mUri, mTrackInfo, mEncryptionData, mProgramDateTime, mHasDiscontinuity, mMapInfo, mByteRange);
}
}
}

View File

@ -0,0 +1,31 @@
package com.iheartradio.m3u8.data;
import java.util.Objects;
public class TrackInfo {
public final float duration;
public final String title;
public TrackInfo(float duration, String title) {
this.duration = duration;
this.title = title;
}
@Override
public int hashCode() {
return Objects.hash(duration, title);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof TrackInfo)) {
return false;
}
TrackInfo other = (TrackInfo) o;
return this.duration == other.duration &&
Objects.equals(this.title, other.title);
}
}

View File

@ -0,0 +1,76 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.Playlist;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import static com.iheartradio.m3u8.TestUtil.inputStreamFromResource;
import static com.iheartradio.m3u8.Constants.UTF_8_BOM_BYTES;
import static org.junit.Assert.*;
public class ByteOrderMarkTest {
@Test
public void testParsingByteOrderMark() throws Exception {
try (final InputStream inputStream = wrapWithByteOrderMark(inputStreamFromResource("simpleMediaPlaylist.m3u8"))) {
final PlaylistParser playlistParser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
final Playlist playlist = playlistParser.parse();
assertEquals(10, playlist.getMediaPlaylist().getTargetDuration());
}
}
@SuppressWarnings("deprecation")
@Test
public void testWritingByteOrderMark() throws Exception {
final Playlist playlist1;
final Playlist playlist2;
final String written;
try (final InputStream inputStream = inputStreamFromResource("simpleMediaPlaylist.m3u8")) {
playlist1 = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8).parse();
}
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
final PlaylistWriter writer = new PlaylistWriter.Builder()
.withOutputStream(os)
.withFormat(Format.EXT_M3U)
.withEncoding(Encoding.UTF_8)
.useByteOrderMark()
.build();
writer.write(playlist1);
written = os.toString(Encoding.UTF_8.value);
}
assertEquals(Constants.UNICODE_BOM, written.charAt(0));
try (final InputStream inputStream = new ByteArrayInputStream(written.getBytes(Encoding.UTF_8.value))) {
playlist2 = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8).parse();
}
assertEquals(playlist1, playlist2);
}
private static InputStream wrapWithByteOrderMark(final InputStream inputStream) {
return new InputStream() {
public int mNumRead;
@Override
public int read() throws IOException {
if (UTF_8_BOM_BYTES.length > mNumRead) {
return UTF_8_BOM_BYTES[mNumRead++];
} else {
return inputStream.read();
}
}
@Override
public void close() throws IOException {
inputStream.close();
}
};
}
}

View File

@ -0,0 +1,53 @@
package com.iheartradio.m3u8;
import org.junit.Test;
public class ExtLineParserTest extends LineParserStateTestCase {
@Test
public void testEXTM3U() throws Exception {
final IExtTagParser handler = ExtLineParser.EXTM3U_HANDLER;
final String tag = Constants.EXTM3U_TAG;
final String line = "#" + tag;
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
assertTrue(mParseState.isExtended());
assertParseThrows(handler, line, ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES);
}
@Test
public void testEXT_X_VERSION() throws Exception {
final IExtTagParser handler = ExtLineParser.EXT_X_VERSION_HANDLER;
final String tag = Constants.EXT_X_VERSION_TAG;
final String line = "#" + tag + ":2";
assertEquals(tag, handler.getTag());
assertParseThrows(handler, line + ".", ParseExceptionType.BAD_EXT_TAG_FORMAT);
handler.parse(line, mParseState);
assertEquals(2, mParseState.getCompatibilityVersion());
assertParseThrows(handler, line, ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES);
}
@Test
public void testEXT_X_START() throws Exception {
final IExtTagParser parser = ExtLineParser.EXT_X_START;
final String tag = Constants.EXT_X_START_TAG;
final String line = "#" + tag +
":TIME-OFFSET=-4.5" +
",PRECISE=YES";
assertEquals(tag, parser.getTag());
assertParseThrows(parser, line + ".", ParseExceptionType.NOT_YES_OR_NO);
parser.parse(line, mParseState);
assertEquals(-4.5, mParseState.startData.getTimeOffset(), 1e-12);
assertEquals(true, mParseState.startData.isPrecise());
assertParseThrows(parser, line, ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES);
}
}

View File

@ -0,0 +1,183 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.MediaData;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.MediaType;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.StreamInfo;
import com.iheartradio.m3u8.data.TrackData;
import com.iheartradio.m3u8.data.TrackInfo;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.iheartradio.m3u8.TestUtil.inputStreamFromResource;
import static org.junit.Assert.*;
public class ExtendedM3uParserTest {
@Test
public void testParseMaster() throws Exception {
final List<MediaData> expectedMediaData = new ArrayList<MediaData>();
expectedMediaData.add(new MediaData.Builder()
.withType(MediaType.AUDIO)
.withGroupId("1234")
.withName("Foo")
.build());
final StreamInfo expectedStreamInfo = new StreamInfo.Builder()
.withBandwidth(500)
.build();
final String validData =
"#EXTM3U\n" +
"#EXT-X-VERSION:2\n" +
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"1234\",NAME=\"Foo\"\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=500\n" +
"http://foo.bar.com/\n" +
"\n";
final InputStream inputStream = new ByteArrayInputStream(validData.getBytes("utf-8"));
final ExtendedM3uParser parser = new ExtendedM3uParser(inputStream, Encoding.UTF_8, ParsingMode.STRICT);
assertTrue(parser.isAvailable());
final Playlist playlist = parser.parse();
assertFalse(parser.isAvailable());
assertTrue(playlist.isExtended());
assertEquals(2, playlist.getCompatibilityVersion());
assertTrue(playlist.hasMasterPlaylist());
assertEquals(expectedMediaData, playlist.getMasterPlaylist().getMediaData());
assertEquals(expectedStreamInfo, playlist.getMasterPlaylist().getPlaylists().get(0).getStreamInfo());
}
@Test
public void testLenientParsing() throws Exception {
final String validData =
"#EXTM3U\n" +
"#EXT-X-VERSION:2\n" +
"#EXT-X-TARGETDURATION:60\n" +
"#EXT-X-MEDIA-SEQUENCE:10\n" +
"#EXT-FAXS-CM:MIIa4QYJKoZIhvcNAQcCoIIa0jCCGs4C...\n" +
"#some comment\n" +
"#EXTINF:120.0,title 1\n" +
"http://www.my.song/file1.mp3\n" +
"\n";
final InputStream inputStream = new ByteArrayInputStream(validData.getBytes("utf-8"));
final Playlist playlist = new ExtendedM3uParser(inputStream, Encoding.UTF_8, ParsingMode.LENIENT).parse();
assertTrue(playlist.isExtended());
assertTrue(playlist.getMediaPlaylist().hasUnknownTags());
assertTrue(playlist.getMediaPlaylist().getUnknownTags().get(0).length() > 0);
}
@Test
public void testParseMedia() throws Exception {
final String absolute = "http://www.my.song/file1.mp3";
final String relative = "user1/file2.mp3";
final String validData =
"#EXTM3U\n" +
"#EXT-X-VERSION:2\n" +
"#EXT-X-TARGETDURATION:60\n" +
"#EXT-X-MEDIA-SEQUENCE:10\n" +
"#some comment\n" +
"#EXTINF:120.0,title 1\n" +
absolute + "\n" +
"#EXTINF:100.0,title 2\n" +
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00\n" +
"\n" +
relative + "\n" +
"\n";
final List<TrackData> expectedTracks = Arrays.asList(
new TrackData.Builder().withUri(absolute).withTrackInfo(new TrackInfo(120, "title 1")).build(),
new TrackData.Builder().withUri(relative).withTrackInfo(new TrackInfo(100, "title 2")).withProgramDateTime("2010-02-19T14:54:23.031+08:00").build());
final InputStream inputStream = new ByteArrayInputStream(validData.getBytes("utf-8"));
final Playlist playlist = new ExtendedM3uParser(inputStream, Encoding.UTF_8, ParsingMode.STRICT).parse();
assertTrue(playlist.isExtended());
assertEquals(2, playlist.getCompatibilityVersion());
assertTrue(playlist.hasMediaPlaylist());
assertEquals(60, playlist.getMediaPlaylist().getTargetDuration());
assertEquals(10, playlist.getMediaPlaylist().getMediaSequenceNumber());
assertEquals(expectedTracks, playlist.getMediaPlaylist().getTracks());
}
@Test
public void testParsingMultiplePlaylists() throws Exception {
try (final InputStream inputStream = inputStreamFromResource("twoMediaPlaylists.m3u8")) {
final PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
assertTrue(parser.isAvailable());
final Playlist playlist1 = parser.parse();
assertTrue(parser.isAvailable());
final Playlist playlist2 = parser.parse();
assertFalse(parser.isAvailable());
List<TrackData> expected1 = Arrays.asList(
makeTrackData("http://media.example.com/first.ts", 9.009f),
makeTrackData("http://media.example.com/second.ts", 9.009f),
makeTrackData("http://media.example.com/third.ts", 3.003f));
assertEquals(
expected1,
playlist1.getMediaPlaylist().getTracks());
assertEquals(
Arrays.asList(
makeTrackData("http://media.example.com/fourth.ts", 9.01f),
makeTrackData("http://media.example.com/fifth.ts", 9.011f)),
playlist2.getMediaPlaylist().getTracks());
assertEquals(0, inputStream.available());
}
}
@Test
public void testParseDiscontinuity() throws Exception {
final String absolute = "http://www.my.song/file1.mp3";
final String relative = "user1/file2.mp3";
final String validData =
"#EXTM3U\n" +
"#EXT-X-VERSION:2\n" +
"#EXT-X-TARGETDURATION:60\n" +
"#EXT-X-MEDIA-SEQUENCE:10\n" +
"#some comment\n" +
"#EXTINF:120.0,title 1\n" +
absolute + "\n" +
"#EXT-X-DISCONTINUITY\n" +
"#EXTINF:100.0,title 2\n" +
"\n" +
relative + "\n" +
"\n";
final InputStream inputStream = new ByteArrayInputStream(validData.getBytes("utf-8"));
final MediaPlaylist mediaPlaylist = new ExtendedM3uParser(inputStream, Encoding.UTF_8, ParsingMode.STRICT).parse().getMediaPlaylist();
assertFalse(mediaPlaylist.getTracks().get(0).hasDiscontinuity());
assertTrue(mediaPlaylist.getTracks().get(1).hasDiscontinuity());
assertEquals(0, mediaPlaylist.getDiscontinuitySequenceNumber(0));
assertEquals(1, mediaPlaylist.getDiscontinuitySequenceNumber(1));
}
private static TrackData makeTrackData(String uri, float duration) {
return new TrackData.Builder()
.withTrackInfo(new TrackInfo(duration, null))
.withUri(uri)
.build();
}
}

View File

@ -0,0 +1,28 @@
package com.iheartradio.m3u8;
import junit.framework.TestCase;
import org.junit.Test;
public class LineParserStateTestCase extends TestCase {
protected ParseState mParseState;
@Override
protected void setUp() throws Exception {
mParseState = new ParseState(Encoding.UTF_8);
}
protected void assertParseThrows(IExtTagParser handler, String line, ParseExceptionType exceptionType) {
try {
handler.parse(line, mParseState);
assertFalse(true);
} catch (ParseException exception) {
assertEquals(exceptionType, exception.type);
}
}
@Test
public void test() {
// workaround for no tests found warning
}
}

View File

@ -0,0 +1,37 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.TrackData;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
public class M3uParserTest {
@Test
public void testParse() throws Exception {
final String absolute = "http://www.my.song/file1.mp3";
final String relative = "user1/file2.mp3";
final String validData =
"#some comment\n" +
absolute + "\n" +
"\n" +
relative + "\n" +
"\n";
final List<TrackData> expectedTracks = Arrays.asList(
new TrackData.Builder().withUri(absolute).build(),
new TrackData.Builder().withUri(relative).build());
final InputStream inputStream = new ByteArrayInputStream(validData.getBytes("utf-8"));
final MediaPlaylist mediaPlaylist = new M3uParser(inputStream, Encoding.UTF_8).parse().getMediaPlaylist();
assertEquals(expectedTracks, mediaPlaylist.getTracks());
}
}

View File

@ -0,0 +1,93 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.MediaData;
import com.iheartradio.m3u8.data.MediaType;
import com.iheartradio.m3u8.data.Resolution;
import com.iheartradio.m3u8.data.StreamInfo;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MasterPlaylistLineParserTest extends LineParserStateTestCase {
@Test
public void testEXT_X_MEDIA() throws Exception {
final List<MediaData> expectedMediaData = new ArrayList<MediaData>();
final IExtTagParser handler = MasterPlaylistLineParser.EXT_X_MEDIA;
final String tag = Constants.EXT_X_MEDIA_TAG;
final String groupId = "1234";
final String language = "lang";
final String associatedLanguage = "assoc-lang";
final String name = "Foo";
final String inStreamId = "SERVICE1";
expectedMediaData.add(new MediaData.Builder()
.withType(MediaType.CLOSED_CAPTIONS)
.withGroupId(groupId)
.withLanguage(language)
.withAssociatedLanguage(associatedLanguage)
.withName(name)
.withAutoSelect(true)
.withInStreamId(inStreamId)
.withCharacteristics(Arrays.asList("char1", "char2"))
.build());
final String line = "#" + tag +
":TYPE=CLOSED-CAPTIONS" +
",GROUP-ID=\"" + groupId + "\"" +
",LANGUAGE=\"" + language + "\"" +
",ASSOC-LANGUAGE=\"" + associatedLanguage + "\"" +
",NAME=\"" + name + "\"" +
",DEFAULT=NO" +
",AUTOSELECT=YES" +
",INSTREAM-ID=\"" + inStreamId + "\"" +
",CHARACTERISTICS=\"char1,char2\"";
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
assertEquals(expectedMediaData, mParseState.getMaster().mediaData);
}
@Test
public void testEXT_X_STREAM_INF() throws Exception {
final IExtTagParser handler = MasterPlaylistLineParser.EXT_X_STREAM_INF;
final String tag = Constants.EXT_X_STREAM_INF_TAG;
final int bandwidth = 10000;
final int averageBandwidth = 5000;
final List<String> codecs = Arrays.asList("h.263", "h.264");
final Resolution resolution = new Resolution(800, 600);
final String audio = "foo";
final String video = "bar";
final String subtitles = "titles";
final String closedCaptions = "captions";
final StreamInfo expectedStreamInfo = new StreamInfo.Builder()
.withBandwidth(bandwidth)
.withAverageBandwidth(averageBandwidth)
.withCodecs(codecs)
.withResolution(resolution)
.withAudio(audio)
.withVideo(video)
.withSubtitles(subtitles)
.withClosedCaptions(closedCaptions)
.build();
final String line = "#" + tag +
":BANDWIDTH=" + bandwidth +
",AVERAGE-BANDWIDTH=" + averageBandwidth +
",CODECS=\"" + codecs.get(0) + "," + codecs.get(1) + "\"" +
",RESOLUTION=" + resolution.width + "x" + resolution.height +
",AUDIO=\"" + audio + "\"" +
",VIDEO=\"" + video + "\"" +
",SUBTITLES=\"" + subtitles + "\"" +
",CLOSED-CAPTIONS=\"" + closedCaptions + "\"";
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
assertEquals(expectedStreamInfo, mParseState.getMaster().streamInfo);
}
}

View File

@ -0,0 +1,24 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class MasterPlaylistParserTest {
@Test
public void test() throws Exception {
final Playlist playlist = TestUtil.parsePlaylistFromResource("masterPlaylist.m3u8");
final MasterPlaylist masterPlaylist = playlist.getMasterPlaylist();
assertTrue(playlist.hasMasterPlaylist());
assertFalse(playlist.hasMediaPlaylist());
assertTrue(masterPlaylist.hasStartData());
assertEquals(4.5, masterPlaylist.getStartData().getTimeOffset(), 1e-12);
assertFalse(masterPlaylist.getStartData().isPrecise());
}
}

View File

@ -0,0 +1,140 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.ByteRange;
import com.iheartradio.m3u8.data.EncryptionData;
import com.iheartradio.m3u8.data.EncryptionMethod;
import com.iheartradio.m3u8.data.MapInfo;
import org.junit.Test;
import java.util.Arrays;
public class MediaPlaylistLineParserTest extends LineParserStateTestCase {
@Test
public void testEXT_X_TARGETDURATION() throws Exception {
final IExtTagParser handler = MediaPlaylistLineParser.EXT_X_TARGETDURATION;
final String tag = Constants.EXT_X_TARGETDURATION_TAG;
final String line = "#" + tag + ":60";
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
assertEquals(60, (int) mParseState.getMedia().targetDuration);
assertParseThrows(handler, line, ParseExceptionType.MULTIPLE_EXT_TAG_INSTANCES);
}
@Test
public void testEXTINF() throws Exception {
final IExtTagParser handler = MediaPlaylistLineParser.EXTINF;
final String tag = Constants.EXTINF_TAG;
final String line = "#" + tag + ":-1,TOP 100";
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
assertEquals(-1f, mParseState.getMedia().trackInfo.duration);
assertEquals("TOP 100", mParseState.getMedia().trackInfo.title);
}
@Test
public void testEXT_X_KEY() throws Exception {
final IExtTagParser handler = MediaPlaylistLineParser.EXT_X_KEY;
final String tag = Constants.EXT_X_KEY_TAG;
final String uri = "http://foo.bar.com/";
final String format = "format";
final String line = "#" + tag +
":METHOD=AES-128" +
",URI=\"" + uri + "\"" +
",IV=0x1234abcd5678EF90aabbccddeeff0011" +
",KEYFORMAT=\"" + format + "\"" +
",KEYFORMATVERSIONS=\"1/2/3\"";
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
EncryptionData encryptionData = mParseState.getMedia().encryptionData;
assertEquals(EncryptionMethod.AES, encryptionData.getMethod());
assertEquals(uri, encryptionData.getUri());
assertEquals(
Arrays.asList((byte) 0x12, (byte) 0x34, (byte) 0xAB, (byte) 0xCD,
(byte) 0x56, (byte) 0x78, (byte) 0xEF, (byte) 0x90,
(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD,
(byte) 0xEE, (byte) 0xFF, (byte) 0x00, (byte) 0x11),
encryptionData.getInitializationVector());
assertEquals(format, encryptionData.getKeyFormat());
assertEquals(Arrays.asList(1, 2, 3), encryptionData.getKeyFormatVersions());
}
@Test
public void testEXT_X_MAP() throws Exception {
final IExtTagParser handler = MediaPlaylistLineParser.EXT_X_MAP;
final String tag = Constants.EXT_X_MAP;
final String uri = "init.mp4";
final long subRangeLength = 350;
final Long offset = 76L;
final String line = "#" + tag +
":URI=\"" + uri + "\"" +
",BYTERANGE=\"" + subRangeLength + "@" + offset + "\"";
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
MapInfo mapInfo = mParseState.getMedia().mapInfo;
assertEquals(uri, mapInfo.getUri());
assertNotNull(mapInfo.getByteRange());
assertEquals(subRangeLength, mapInfo.getByteRange().getSubRangeLength());
assertEquals(offset, mapInfo.getByteRange().getOffset());
}
@Test
public void testEXT_X_BYTERANGE() throws Exception {
final IExtTagParser handler = MediaPlaylistLineParser.EXT_X_BYTERANGE;
final String tag = Constants.EXT_X_BYTERANGE_TAG;
final long subRangeLength = 350;
final Long offset = 70L;
final String line = "#" + tag + ":" + subRangeLength + "@" + offset;
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
ByteRange byteRange = mParseState.getMedia().byteRange;
assertEquals(subRangeLength, byteRange.getSubRangeLength());
assertEquals(offset, byteRange.getOffset());
}
@Test
public void testEXT_X_PROGRAM_DATE_TIME_withPositiveOffset() throws Exception {
testProgramDateTime("2021-03-20T17:04:20.694+0245");
}
@Test
public void testEXT_X_PROGRAM_DATE_TIME_withNegativeOffset() throws Exception {
testProgramDateTime("2021-03-20T17:04:20.694-0300");
}
@Test
public void testEXT_X_PROGRAM_DATE_TIME_withPositiveShortOffset() throws Exception {
testProgramDateTime("2021-03-20T17:04:20.694+02");
}
@Test
public void testEXT_X_PROGRAM_DATE_TIME_withPositiveOffsetAndColon() throws Exception {
testProgramDateTime("2021-03-20T17:04:20.694+02:45");
}
@Test
public void testEXT_X_PROGRAM_DATE_TIME_withZ() throws Exception {
testProgramDateTime("2021-03-20T17:04:20.694Z");
}
private void testProgramDateTime(String dateTime) throws ParseException {
final String tag = Constants.EXT_X_PROGRAM_DATE_TIME_TAG;
IExtTagParser handler = MediaPlaylistLineParser.EXT_X_PROGRAM_DATE_TIME;
String line = "#" + tag + ":" + dateTime;
assertEquals(tag, handler.getTag());
handler.parse(line, mParseState);
}
}

View File

@ -0,0 +1,24 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class MediaPlaylistParserTest {
@Test
public void test() throws Exception {
final Playlist playlist = TestUtil.parsePlaylistFromResource("mediaPlaylist.m3u8");
final MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
assertFalse(playlist.hasMasterPlaylist());
assertTrue(playlist.hasMediaPlaylist());
assertTrue(mediaPlaylist.hasStartData());
assertEquals(-4.5, mediaPlaylist.getStartData().getTimeOffset(), 1e-12);
assertTrue(mediaPlaylist.getStartData().isPrecise());
assertEquals(10, mediaPlaylist.getTargetDuration());
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2017, Spiideo
*/
package com.iheartradio.m3u8;
import java.util.Arrays;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
/**
* @author Raniz
* @since 02/08/17.
*/
@RunWith(Parameterized.class)
public class ParseUtilParseHexadecimalTest {
@Parameterized.Parameters(name = "{index}: {1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Arrays.asList((byte) 0), "0x00"},
{Arrays.asList((byte) 1), "0x01"},
{Arrays.asList((byte) -1), "0xff"},
{Arrays.asList((byte) -16), "0xf0"},
{Arrays.asList((byte) 0, (byte) 1), "0x0001"},
{Arrays.asList((byte) 1, (byte) 1), "0x0101"},
{Arrays.asList((byte) -1, (byte) -1), "0xffff"},
{Arrays.asList((byte) -121, (byte) -6), "0x87fa"},
{Arrays.asList((byte) 75, (byte) 118), "0x4b76"},
});
}
private final List<Byte> expected;
private final String input;
public ParseUtilParseHexadecimalTest(final List<Byte> expected, final String input) {
this.expected = expected;
this.input = input;
}
@Test
public void parseHexadecimal() throws Exception {
Assert.assertEquals(expected, ParseUtil.parseHexadecimal(input, ""));
}
}

View File

@ -0,0 +1,217 @@
package com.iheartradio.m3u8;
import static org.junit.Assert.*;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import com.iheartradio.m3u8.data.ByteRange;
import com.iheartradio.m3u8.data.IFrameStreamInfo;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.TrackData;
public class PlaylistParserWriterTest {
Playlist readPlaylist(String fileName) throws IOException, ParseException, PlaylistException {
assertNotNull(fileName);
try(InputStream is = new FileInputStream("src/test/resources/" + fileName)) {
Playlist playlist = new PlaylistParser(is, Format.EXT_M3U, Encoding.UTF_8).parse();
return playlist;
}
}
String writePlaylist(Playlist playlist) throws IOException, ParseException, PlaylistException {
assertNotNull(playlist);
try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {
PlaylistWriter writer = new PlaylistWriter(os, Format.EXT_M3U, Encoding.UTF_8);
writer.write(playlist);
return os.toString(Encoding.UTF_8.value);
}
}
@Test
public void simpleMediaPlaylist() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("simpleMediaPlaylist.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println(sPlaylist);
}
@Test
public void liveMediaPlaylist() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("liveMediaPlaylist.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println(sPlaylist);
}
@Test
public void playlistWithEncryptedMediaSegments() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("playlistWithEncryptedMediaSegments.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println(sPlaylist);
}
@Test
public void masterPlaylist() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("masterPlaylist.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println(sPlaylist);
}
@Test
public void masterPlaylistWithIFrames() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("masterPlaylistWithIFrames.m3u8");
assertTrue(playlist.hasMasterPlaylist());
MasterPlaylist masterPlaylist = playlist.getMasterPlaylist();
assertNotNull(masterPlaylist);
List<PlaylistData> playlistDatas = masterPlaylist.getPlaylists();
List<IFrameStreamInfo> iFrameInfo = masterPlaylist.getIFramePlaylists();
assertNotNull(playlistDatas);
assertNotNull(iFrameInfo);
assertEquals(4, playlistDatas.size());
assertEquals(3, iFrameInfo.size());
PlaylistData lowXStreamInf = playlistDatas.get(0);
assertNotNull(lowXStreamInf);
assertNotNull(lowXStreamInf.getStreamInfo());
assertEquals(1280000, lowXStreamInf.getStreamInfo().getBandwidth());
assertEquals("low/audio-video.m3u8", lowXStreamInf.getUri());
PlaylistData midXStreamInf = playlistDatas.get(1);
assertNotNull(midXStreamInf);
assertNotNull(midXStreamInf.getStreamInfo());
assertEquals(2560000, midXStreamInf.getStreamInfo().getBandwidth());
assertEquals("mid/audio-video.m3u8", midXStreamInf.getUri());
PlaylistData hiXStreamInf = playlistDatas.get(2);
assertNotNull(hiXStreamInf);
assertNotNull(hiXStreamInf.getStreamInfo());
assertEquals(7680000, hiXStreamInf.getStreamInfo().getBandwidth());
assertEquals("hi/audio-video.m3u8", hiXStreamInf.getUri());
PlaylistData audioXStreamInf = playlistDatas.get(3);
assertNotNull(audioXStreamInf);
assertNotNull(audioXStreamInf.getStreamInfo());
assertEquals(65000, audioXStreamInf.getStreamInfo().getBandwidth());
assertNotNull(audioXStreamInf.getStreamInfo().getCodecs());
assertEquals(1, audioXStreamInf.getStreamInfo().getCodecs().size());
assertEquals("mp4a.40.5", audioXStreamInf.getStreamInfo().getCodecs().get(0));
assertEquals("audio-only.m3u8", audioXStreamInf.getUri());
IFrameStreamInfo lowXIFrameStreamInf = iFrameInfo.get(0);
assertNotNull(lowXIFrameStreamInf);
assertEquals(86000, lowXIFrameStreamInf.getBandwidth());
assertEquals("low/iframe.m3u8", lowXIFrameStreamInf.getUri());
IFrameStreamInfo midXIFrameStreamInf = iFrameInfo.get(1);
assertNotNull(midXIFrameStreamInf);
assertEquals(150000, midXIFrameStreamInf.getBandwidth());
assertEquals("mid/iframe.m3u8", midXIFrameStreamInf.getUri());
IFrameStreamInfo hiXIFrameStreamInf = iFrameInfo.get(2);
assertNotNull(hiXIFrameStreamInf);
assertEquals(550000, hiXIFrameStreamInf.getBandwidth());
assertEquals("hi/iframe.m3u8", hiXIFrameStreamInf.getUri());
String writtenPlaylist = writePlaylist(playlist);
assertEquals(
"#EXTM3U\n" +
"#EXT-X-VERSION:1\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=1280000\n" +
"low/audio-video.m3u8\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=2560000\n" +
"mid/audio-video.m3u8\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=7680000\n" +
"hi/audio-video.m3u8\n" +
"#EXT-X-STREAM-INF:CODECS=\"mp4a.40.5\",BANDWIDTH=65000\n" +
"audio-only.m3u8\n" +
"#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n" +
"#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n" +
"#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n",
writtenPlaylist);
}
@Test
public void masterPlaylistWithAlternativeAudio() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("masterPlaylistWithAlternativeAudio.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println(sPlaylist);
}
@Test
public void masterPlaylistWithAlternativeVideo() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("masterPlaylistWithAlternativeVideo.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println(sPlaylist);
}
@Test
public void discontinutyPlaylist() throws IOException, ParseException, PlaylistException {
Playlist playlist = readPlaylist("withDiscontinuity.m3u8");
String sPlaylist = writePlaylist(playlist);
System.out.println("***************");
System.out.println(sPlaylist);
}
@Test
@Ignore
public void playlistWithByteRanges() throws Exception {
final Playlist playlist = TestUtil.parsePlaylistFromResource("mediaPlaylistWithByteRanges.m3u8");
final MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
List<ByteRange> byteRanges = new ArrayList<>();
for (TrackData track : mediaPlaylist.getTracks()) {
assertTrue(track.hasByteRange());
byteRanges.add(track.getByteRange());
}
List<ByteRange> expected = Arrays.asList(
new ByteRange(0, 10),
new ByteRange(20),
new ByteRange(30)
);
assertEquals(expected, byteRanges);
assertEquals(
"#EXTM3U\n" +
"#EXT-X-VERSION:4\n" +
"#EXT-X-TARGETDURATION:10\n" +
"#EXT-X-MEDIA-SEQUENCE:0\n"+
"#EXT-X-BYTERANGE:0@10\n" +
"#EXTINF:9.009\n" +
"http://media.example.com/first.ts\n" +
"#EXT-X-BYTERANGE:20\n" +
"#EXTINF:9.009\n" +
"http://media.example.com/first.ts\n" +
"#EXT-X-BYTERANGE:30\n" +
"#EXTINF:3.003\n" +
"http://media.example.com/first.ts\n" +
"#EXT-X-ENDLIST\n", writePlaylist(playlist));
}
}

View File

@ -0,0 +1,46 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.Playlist;
import org.junit.Test;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.iheartradio.m3u8.TestUtil.inputStreamFromResource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PlaylistValidationTest {
@Test
public void testAllowNegativeNumbersValidation() throws Exception {
Playlist playlist;
boolean found = false;
try (final InputStream inputStream = inputStreamFromResource("negativeDurationMediaPlaylist.m3u8")) {
new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8).parse();
} catch (final PlaylistException exception) {
found = exception.getErrors().contains(PlaylistError.TRACK_INFO_WITH_NEGATIVE_DURATION);
}
assertTrue(found);
try (final InputStream inputStream = inputStreamFromResource("negativeDurationMediaPlaylist.m3u8")) {
playlist = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT).parse();
}
assertEquals(-1f, playlist.getMediaPlaylist().getTracks().get(0).getTrackInfo().duration, 0f);
}
@Test
public void testInvalidBytRange() throws Exception {
List<PlaylistError> errors = new ArrayList<>();
try {
TestUtil.parsePlaylistFromResource("mediaPlaylistWithInvalidByteRanges.m3u8");
} catch (PlaylistException e) {
errors.addAll(e.getErrors());
}
assertEquals(Collections.singletonList(PlaylistError.BYTERANGE_WITH_UNDEFINED_OFFSET), errors);
}
}

View File

@ -0,0 +1,30 @@
package com.iheartradio.m3u8;
import com.iheartradio.m3u8.data.Playlist;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertNotNull;
public class TestUtil {
public static InputStream inputStreamFromResource(final String fileName) {
assertNotNull(fileName);
try {
return new FileInputStream("src/test/resources/" + fileName);
} catch (FileNotFoundException e) {
throw new RuntimeException("failed to open playlist file: " + fileName);
}
}
public static Playlist parsePlaylistFromResource(final String fileName) throws IOException, ParseException, PlaylistException {
assertNotNull(fileName);
try (InputStream is = new FileInputStream("src/test/resources/" + fileName)) {
return new PlaylistParser(is, Format.EXT_M3U, Encoding.UTF_8).parse();
}
}
}

View File

@ -0,0 +1,27 @@
package com.iheartradio.m3u8;
import org.junit.Test;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;
public class WriteUtilTest {
@Test
public void writeQuotedStringShouldIgnoreNullTagValueForOptionalFields() throws Exception {
String outputString =WriteUtil.writeQuotedString(null,"some-key", true);
assertThat(outputString,is("\"\""));
}
@Test(expected = NullPointerException.class)
public void writeQuotedStringShouldNotIgnoreNullTagValue() throws Exception {
WriteUtil.writeQuotedString(null,"some-key");
}
@Test
public void writeQuotedStringShouldNotIgnoreSuppliedOptionalValue() throws Exception {
assertThat(WriteUtil.writeQuotedString("blah","some-key"),is("\"blah\""));
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2017, Spiideo
*/
package com.iheartradio.m3u8;
import java.util.Arrays;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
/**
* @author Raniz
* @since 02/08/17.
*/
@RunWith(Parameterized.class)
public class WriteUtilWriteHexadecimalTest {
@Parameterized.Parameters(name = "{index}: {1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Arrays.asList((byte) 0), "0x00"},
{Arrays.asList((byte) 1), "0x01"},
{Arrays.asList((byte) -1), "0xff"},
{Arrays.asList((byte) -16), "0xf0"},
{Arrays.asList((byte) 0, (byte) 1), "0x0001"},
{Arrays.asList((byte) 1, (byte) 1), "0x0101"},
{Arrays.asList((byte) -1, (byte) -1), "0xffff"},
{Arrays.asList((byte) -121, (byte) -6), "0x87fa"},
{Arrays.asList((byte) 75, (byte) 118), "0x4b76"},
});
}
private final List<Byte> input;
private final String expected;
public WriteUtilWriteHexadecimalTest(final List<Byte> input, final String expected) {
this.input = input;
this.expected = expected;
}
@Test
public void writeHexadecimal() throws Exception {
Assert.assertEquals(expected, WriteUtil.writeHexadecimal(input));
}
}

View File

@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680
#EXTINF:7.975,
https://priv.example.com/fileSequence2680.ts
#EXTINF:7.941,
https://priv.example.com/fileSequence2681.ts
#EXTINF:7.975,
https://priv.example.com/fileSequence2682.ts

View File

@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-START:TIME-OFFSET=4.5,PRECISE=NO
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000
http://example.com/low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000
http://example.com/hi.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
http://example.com/audio-only.m3u8

View File

@ -0,0 +1,12 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="2",URI="main/english-audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",CHANNELS="2",URI="main/german-audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",CHANNELS="2",URI="commentary/audio-only.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",AUDIO="aac"
low/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",AUDIO="aac"
mid/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",AUDIO="aac"
hi/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac"
main/english-audio.m3u8

View File

@ -0,0 +1,24 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",VIDEO="low"
low/main/audio-video.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",VIDEO="mid"
mid/main/audio-video.m3u8
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",VIDEO="hi"
hi/main/audio-video.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
main/audio-only.m3u8

View File

@ -0,0 +1,12 @@
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1280000
low/audio-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2560000
mid/audio-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=7680000
hi/audio-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
audio-only.m3u8

View File

@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-START:TIME-OFFSET=-4.5,PRECISE=YES
#EXT-X-TARGETDURATION:10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST

View File

@ -0,0 +1,13 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXT-X-BYTERANGE:0@10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXT-X-BYTERANGE:20
#EXTINF:9.009,
http://media.example.com/first.ts
#EXT-X-BYTERANGE:30
#EXTINF:3.003,
http://media.example.com/first.ts
#EXT-X-ENDLIST

View File

@ -0,0 +1,13 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXT-X-BYTERANGE:0@10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXT-X-BYTERANGE:20
#EXTINF:9.009,
http://media.example.com/second.ts
#EXT-X-BYTERANGE:30
#EXTINF:3.003,
http://media.example.com/first.ts
#EXT-X-ENDLIST

View File

@ -0,0 +1,7 @@
#EXTM3U
#EXTINF:-1,TOP 100
http://radio.promodj.com:8000/top100-192
#EXTINF:-1,Channel 5
http://radio.promodj.com:8000/channel5-192
#EXTINF:-1,Klubb
http://radio.promodj.com:8000/klubb-192

View File

@ -0,0 +1,18 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:7794
#EXT-X-TARGETDURATION:15
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
#EXTINF:2.833,
http://media.example.com/fileSequence52-A.ts
#EXTINF:15.0,
http://media.example.com/fileSequence52-B.ts
#EXTINF:13.333,
http://media.example.com/fileSequence52-C.ts
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53"
#EXTINF:15.0,
http://media.example.com/fileSequence53-A.ts

View File

@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST

View File

@ -0,0 +1,15 @@
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:9.009
http://media.example.com/first.ts
#EXTINF:9.009
http://media.example.com/second.ts
#EXTINF:3.003
http://media.example.com/third.ts
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:9.010
http://media.example.com/fourth.ts
#EXTINF:9.011
http://media.example.com/fifth.ts

View File

@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10,Advertisement
http://myserver/api/data/params?pubid=e4ac7b50-0422-4938-be6a-b0f49c50ebfc&chname=sample12.mp4&profid=1&slotid=1&pcrpts=0&ssid=<1234567890123456>&breakid=1&contenturl=
#EXTINF:10,Advertisement
http://myserver/api/data/params?pubid=e4ac7b50-0422-4938-be6a-b0f49c50ebfc&chname=sample12.mp4&profid=1&slotid=2&pcrpts=0&ssid=<1234567890123456>&breakid=1&contenturl=
#EXTINF:10,Advertisement
http://myserver/api/data/params?pubid=e4ac7b50-0422-4938-be6a-b0f49c50ebfc&chname=sample12.mp4&profid=1&slotid=3&pcrpts=0&ssid=<1234567890123456>&breakid=1&contenturl=
#EXT-X-DISCONTINUITY
#EXTINF:10
http://contentserver/vod/sample.mp4/media_w1013470664_0.ts
#EXTINF:10
http://contentserver/vod/sample.mp4/media_w1013470664_1.ts
#EXT-X-ENDLIST