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