Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Java CI

on:
push:
tags: ['*.*.*']

jobs:
build:
runs-on: ubuntu-latest

permissions:
id-token: write # This is required for requesting the JWT
contents: write
discussions: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'temurin'
# TODO (mod-squad): Add S3 fetching routine to pull in WSE dependencies.
- name: Configure AWS credentials for hub account
uses: aws-actions/configure-aws-credentials@v3
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.HUB_ACCOUNT_ROLE_ARN }}
role-session-name: ${{ secrets.HUB_ACCOUNT_ROLE_SESSION_NAME }}
- name: Configure AWS Credentials for spoke account
uses: aws-actions/configure-aws-credentials@v3
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.SPOKE_ACCOUNT_ROLE_ARN }}
role-session-name: ${{ secrets.SPOKE_ACCOUNT_ROLE_SESSION_NAME }}
role-chaining: true
role-skip-session-tagging: true
- name: Copy WSE distribution from S3
run: |
aws s3 cp s3://${{ secrets.WOWZA_DISTRIBUTION_BUCKET }}/${{ vars.WOWZA_RELEASE_CHANNEL }}/${{ vars.WOWZA_VERSION }}/WowzaStreamingEngine-Update-${{ vars.WOWZA_VERSION }}.zip .
- name: Unzip WSE distribution
run: |
unzip WowzaStreamingEngine-Update-${{ vars.WOWZA_VERSION }}.zip -d ${{ vars.WSE_HOME }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v4
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build Gradle
run: gradle build -Pversion=${{ github.ref_name }} -PwseLibDir=${{ vars.WSE_HOME }}/files/lib
- name: Release
uses: softprops/action-gh-release@v2
with:
discussion_category_name: announcements
generate_release_notes: true
files: |
build/libs/*
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.gradle
**/build/
!src/**/build/

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties

# Cache of project
.gradletasknamecache

# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath

.env
wse
model_cache
234 changes: 234 additions & 0 deletions LICENSE.txt

Large diffs are not rendered by default.

140 changes: 139 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,139 @@
# wse-plugin-interstitials-rest-api
# Wowza Streaming Engine HLS interstitials REST API

With the **HLS Interstitials REST API** module for [Wowza Streaming Engine™ media server software](https://www.wowza.com/products/streaming-engine), you can use a REST API to add HLS interstitials to a live video feed by inserting an `#EXT-DATE-RANGE` tag in the HLS manifest.

For more details about HLS interstitials, see [Getting Started with HLS Interstitials](https://developer.apple.com/streaming/GettingStartedWithHLSInterstitials.pdf).

## Prerequisites

* Wowza Streaming Engine™ 4.9.4 or later is required
* Java 21
* Gradle (to build)

## Build instructions

1. Clone this repository to your local filesystem.
2. Update the `wseLibDir` variable in the `gradle.properties` file to point to the local Wowza Streaming Engine `lib` folder.
3. Run `./gradlew build` to build the jar file.

## Install

1. Copy `wse-plugin-cloud-interstitials-rest-api-x.x.x.jar` into the `lib` directory.
2. Add the HTTPProvider to `VHost.xml`:

```xml
<HTTPProvider>
<BaseClass>com.wowza.wms.plugin.interstitialsrestapi.http.HTTPProviderInterstitialsRestApi</BaseClass>
<RequestFilters>v1/interstitials/*</RequestFilters>
<AuthenticationMethod>none</AuthenticationMethod>
</HTTPProvider>
```

3. Add the following property to `VHost.xml`:

```xml
<Property>
<Name>optionsCORSHeadersAddMain</Name>
<Value>Access-Control-Allow-Methods:DELETE</Value>
<Type>String</Type>
</Property>
```

4. Add the following module to the Application.xml:

```xml
<Module>
<Name>ModuleInterstitialsRestApi</Name>
<Description>ModuleInterstitialsRestApi</Description>
<Class>com.wowza.wms.plugin.interstitialsrestapi.module.ModuleInterstitialsRestApi</Class>
</Module>
```

5. Add the following property to the `HTTPStreamer > Properties` block in the Application.xml file:

```xml
<Property>
<Name>cupertinoEnableProgramDateTime</Name>
<Value>true</Value>
<Type>Boolean</Type>
</Property>
```

## API details

### API pattern

* `/v1/interstitials/applications/{appName}/streams/{streamName}`

### API supported methods

* `POST`
* `DELETE`

### Metadata

A JSON object can be passed into the video stream using the properties outlined in the following table.

#### Properties

| Property | Description |
| :-------------- | :----------------------------------------------------------------------- |
| `id` | Specify an identifier to use for the ad. Default value is `ad1`. |
| `start_date` | Define an absolute start date in ISO8601 format, or +seconds from now. Defaults to the current time. |
| `duration` | Specify duration for the ad. Default value is 30 seconds. |
| `asset_list` | Define a URL for an assets list. If not defined, must have `asset_uri`. |
| `asset_uri` | Define a URL for a single asset. If not defined, must have `asset_list`. |
| `resume_offset` | Determine when primary playback should resume following the playback of the interstitial. Default value is 0 seconds. |
| `restrict` | Create a list of navigation restrictions. Default value is `SKIP,JUMP`. |

## Examples and demo

After building the module, start Wowza Streaming Engine and Wowza Streaming Engine Manager using the docker-compose.yaml file in this repository. It includes a pre-configured Wowza Streaming Engine instance and sample `live` and `simu-live` applications.

1. Run the following command:

```bash
docker compose up
```

2. Insert an HLS interstitial for the `simu-live` application with a 10 second ad break, five seconds from now:

```shell
curl -X POST -H "Content-Type: application/json" -d '{
"id": "ad1",
"start_date": "+5",
"duration": 10.0,
"asset_uri": "https://wv-cdn-00-00.flowplayer.com/7bb18344-08f9-4c1e-84a7-80c1007aa99b/cmaf/6089d839-d699-424b-b914-445152e25115/playlist.m3u8"
}' http://localhost/v1/interstitials/applications/simu-live/streams/myStream
```

3. To test playback, go to:

```text
https://hlsjs.video-dev.org/demo/?src=https://wse-trial.wowza.com/simu-live/myStream/playlist.m3u8
```

4. To view the HLS interstitial in the HLS manifest, run:

```bash
curl http://localhost/simu-live/myStream/chunklist_w2003968828.m3u8
```

## HLS output example

An HLS output example looks similar to:

```text
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:60897
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-PROGRAM-DATE-TIME:2025-02-13T17:03:19.368Z
#EXT-X-DATERANGE:ID="ad1-5",CLASS="com.apple.hls.interstitial",START-DATE="2025-06-30T20:14:26.497Z",DURATION=10.000,X-ASSET-URI="https://wv-cdn-00-00.flowplayer.com/7bb18344-08f9-4c1e-84a7-80c1007aa99b/cmaf/6089d839-d699-424b-b914-445152e25115/playlist.m3u8",X-RESUME-OFFSET=0,X-RESTRICT="SKIP,JUMP"
#EXTINF:4.0,
media_10.ts
#EXTINF:4.0,
media_11.ts
#EXTINF:4.0,
media_12.ts
111 changes: 111 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

plugins {
id 'java-library'
id "com.gorylenko.gradle-git-properties" version "2.4.0-rc1"
}

group 'com.wowza.wms.plugin.interstitialsrestapi'
version '1.0.0'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

repositories {
mavenCentral()
flatDir {
dirs "$wseLibDir"
}
}

configurations {
plugin
compileClasspath.extendsFrom(plugin)
runtimeClasspath.extendsFrom(plugin)
testCompileClasspath.extendsFrom(plugin)
testRuntimeClasspath.extendsFrom(plugin)
}

dependencies {
implementation name: 'wms-server'
implementation name: 'wms-stream-live'
implementation name: 'wms-stream-publish'
implementation name: 'wms-transcoder'
implementation name: 'wms-httpstreamer-cupertinostreaming'
implementation name: 'wms-httpstreamer-mpegdashstreaming'
implementation 'org.apache.logging.log4j:log4j-core:2.17.2'
implementation 'org.apache.logging.log4j:log4j-api:2.17.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

tasks.register('copyDeps', Copy) {
from (configurations.plugin)
into layout.buildDirectory.dir("libs")
}

jar.configure {
dependsOn (copyDeps)
}

tasks.withType(Jar).configureEach {
manifest {
attributes(
'Gradle-Version' : "Gradle ${gradle.gradleVersion}",
'Created-By' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
'Name' : "${project.name}",
'Build-Version' : "${project.version}",
'Build-Timestamp' : "${-> project.ext.gitProps['git.commit.time']}",
'Build-Revision' : "${-> project.ext.gitProps['git.commit.id.abbrev']}",
)
}
exclude('git.properties')
metaInf {
from files('LICENSE.txt')
}
archiveBaseName = 'wse-plugin-' + projectName
}

gitProperties {
extProperty = 'gitProps'
dateFormat = "EEE LLL dd HH:mm:ss yyyy Z"
dateFormatTimeZone = "UTC"
}

generateGitProperties.outputs.upToDateWhen { false }

tasks.register('generateReleaseInfo') {
dependsOn generateGitProperties
group = "build"
Provider<Directory> outputDir = layout.buildDirectory.dir('generated/java') as Provider<Directory>
outputs.dir outputDir.get().asFile.absolutePath
doLast {
def now = System.currentTimeMillis()
def packageDotPath = "${project.group}"
def packagePath = packageDotPath.replaceAll('\\.', '/')
Directory dir = outputDir.get().dir(packagePath)
dir.asFile.mkdirs()
dir.file("ReleaseInfo.java").asFile.text =
"""|package $packageDotPath;
|public class ReleaseInfo {
| public static String getProject() { return "${projectName}"; }
| public static String getVersion() { return "${project.version}"; }
| public static String getBuildComitDate() { return "${-> project.ext.gitProps['git.commit.time']}"; }
| public static String getBuildNumber() { return "${-> project.ext.gitProps['git.commit.id.abbrev']}"; }
|}""".stripMargin()
}
}

sourceSets.main.java.srcDir generateReleaseInfo
clean.finalizedBy generateReleaseInfo
compileJava.dependsOn generateReleaseInfo

test {
useJUnitPlatform()
}
Loading