For Reach Engine 2.3.2 support has been added to stream Ad-hoc Live Events from HTTP Live Stream .m3u8 files. This work was done for Jira ticket "REACH-27323: As a live event consumer, I would like my live event to pull/set the start time of the live event from the m3u8 manifest."
In order for the live stream's start-time to displayed in the video player the .m3u8 file must be parsed and the value from "EXT-X-PROGRAM-DATE-TIME" line returned to Reach Engine. Rather than a minimalist manual parse to pick out just that one start-time value we decided to use an open source parser library. This approach enables future versions of Reach Engine to easily extract more information from the .m3u8 files.
- Added a Maven pom.xml file.
- In
Constants.java
the MAX_COMPATIBILITY_VERSION was increased to "7" because Telestream outputs that version in their files. - Added 2 sample .m3u8 files with EXT-X-PROGRAM-DATE-TIME specified and a test case:
MediaPlaylistParserStartTimeTest.java
- Commented out a unit test in
PlaylistParserWriterTest.java
that was failing. - In
build.gradle
the artifact group was changed tocom.levelsbeyond
and the version was changed to0.2.7
.
A Maven pom.xml was written to enable a regular build process. It has the Levels Beyond <scm>
and <repositories>
declarations copied from a Reach Engine pom.
An older version of Gradle, version 2.4 is required to build the open-3u8 library. On macOS older versions are installed with the SDKMAN tool.
Here is a good article describing how to do this:
https://medium.com/@czerwinb/how-to-install-a-specific-gradle-version-on-your-mac-beab35051ee8
Here are the command lines needed:
curl -s "https://get.sdkman.io" | bash
cd ~/.sdkman/bin/
chmod +x sdkman-init.sh
source sdkman-init.sh
sdk list gradle
sdk install gradle 2.4
sdk use gradle 2.4
sdk default gradle 2.4
For the initial release the open-m3u8-0.2.7.jar
file must be manually uploaded to the Levels Beyond Artifactory repository.
Here is how it is declared as a dependency in the reach-engine-core module's pom.xml:
<dependency>
<groupId>com.levelsbeyond</groupId>
<artifactId>open-m3u8</artifactId>
<version>0.2.7</version>
</dependency>
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!
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.
We now have artifacts in Maven Central! Artifacts are typically built with Java 7.
dependencies {
compile 'com.iheartradio.m3u8:open-m3u8:0.2.4'
}
<dependency>
<groupId>com.iheartradio.m3u8</groupId>
<artifactId>open-m3u8</artifactId>
<version>0.2.4</version>
</dependency>
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.
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:
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:
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.
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.
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.
ParsingMode.LENIENT // lenient about everything
ParsingMode.STRICT // strict about everything
Example:
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");
}
=======
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