initial
This commit is contained in:
commit
b34f82bf6a
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
|
@ -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`
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
|
||||
interface AttributeParser<Builder> {
|
||||
void parse(Attribute attribute, Builder builder, ParseState state) throws ParseException;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
interface AttributeWriter<T> {
|
||||
|
||||
String write(T attributes) throws ParseException;
|
||||
|
||||
boolean containsAttribute(T attributes);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
public enum Format {
|
||||
M3U,
|
||||
EXT_M3U;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
interface IExtTagParser extends LineParser {
|
||||
String getTag();
|
||||
boolean hasData();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
interface IExtTagWriter extends SectionWriter {
|
||||
String getTag();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
interface IParseState<T> {
|
||||
T buildPlaylist() throws ParseException;
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.iheartradio.m3u8;
|
||||
|
||||
interface LineParser {
|
||||
void parse(String line, ParseState state) throws ParseException;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + "]";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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, ""));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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\""));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue