Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HLS Interstitials Support #6591

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open

HLS Interstitials Support #6591

wants to merge 21 commits into from

Conversation

robwalch
Copy link
Collaborator

@robwalch robwalch commented Jul 30, 2024

This PR will...

Add support for HLS Interstitials parsing, playback, and events.

Design Details

The InterstitialsController handles HLS and media input and events for the primary HLS asset. It passes date range data to the scheduler to produce an array of Interstitial events and an array of event and primary items called the schedule. InterstitialsController has:

  • an InterstitialsSchedule which produces Interstitial events and a schedule of items on level update. The InterstitialsSchedule has
    • an array of InterstitialEvent objects. Each event contains properties and getters for finding its place on the playback timeline as well as an array of interstitial assets
  • an array of HLSAssetPlayer instances call the player queue. Each HLSAssetPlayer wraps a child instance of Hls used to preload and stream Interstitial assets.
  • the responsibility of loading asset lists. The InterstitialsController adds a loader to the corresponding InterstitialEvent while loading. The loader is removed and replaced by the asset list response once the request is complete.
  • the responsibility of scheduling Interstitial playback and maintaining the current playing and buffering schedule items and assets. The schedule advances according to primary currentTime changes on timeupdate and seeking events, advancement of the combined buffer on media append, and asset playback and buffer advancement and completion.

API Enhancements

Configuration options

  • Interstitials parsing and playback are enabled via config option interstitialsController. Setting this to null turns off Interstitial parsing and playback.
  • To turn off Interstitial playback without removing parsing or schedule updates and buffered-to events, set config option enableInterstitialPlayback to false (allowing for custom playout and ad managers)
  • Config option interstitialAssetListLoadPolicy defines the loading policy of X-ASSET-LIST JSON
  • Several config options have been added specifically for Interstitial asset player instances:
    • primarySessionId identifies the parent player session that spawned the asset player (read from hls.sessionId)
    • assetPlayerId is used to identify logs from asset players
    • timelineOffset is used to offset MSE appends of Interstitial content (not all Interstitial assets are appended inline at offsets; most require a MediaSource reset)

New top-level API on Hls instances

  • bufferedToEnd getter returns a boolean indicating if EOS has been appended (media is buffered from currentTime to end of stream)
  • bufferingEnabled getter returns a boolean indicating whether fragment loading has been toggled with pauseBuffering() and resumeBuffering()
  • interstitialsManager getter returns an InterstitialsManager (or null). The InterstitialsManager is an interface that provides access to Interstitial program and timeline data as well as methods for seeking across items and skipping Interstitial events.
  • latestLevelDetails getter returns the LevelDetails of the most up-to-date HLS variant Playlist data
  • sessionId getter returns the session UUID assigned to the Hls instance
    • The sessionId value is used when assigning a _HLS_primary_id query parameter to interstitial requests
  • startLoad() now includes a second optional argument to skip seeking on start (otherwise, HLS.js seeks following to the first optional startPosition argument on append)
  • hasEnoughToStart getter returns whether enough is buffered to seek to start position (fix: media buffer is not empty but video element buffer is empty #6571)
  • startPosition getter returns the resolved startPosition that playback will begin at once media is appended
  • transferMedia() method detaches and returns MediaSource and SourceBuffers non-destructively
  • url getter returns the URL resolved from loadSource(url)

Updated top-level API on Hls instances

New Events for Interstitials

  • ASSET_LIST_LOADING when a request is made for an X-ASSET-LIST JSON object
  • ASSET_LIST_LOADED when a response is received for an X-ASSET-LIST JSON object
  • INTERSTITIALS_UPDATED when Interstitials are added, removed, or the schedule is updated following a variant playlist update or updated asset durations from X-ASSET-LIST JSON or asset playlist and media parsing
  • INTERSTITIALS_BUFFERED_TO_BOUNDARY when the forward buffer reaches the boundary of the following schedule item (Interstitial event or primary segment)
  • INTERSTITIAL_ASSET_PLAYER_CREATED when an asset player instance is created to stream an Interstitial asset (will always be before attaching media to the asset player)
  • INTERSTITIAL_STARTED when streaming of an Interstitial event containing one or more assets has begun (may occur before X-ASSET-LIST JSON is loaded or playback has started)
  • INTERSTITIAL_ENDED when streaming of an Interstitial event containing one or more assets has ended - before resuming primary or starting the next event
  • INTERSTITIAL_ASSET_STARTED when streaming of an Interstitial asset has begun (following the beginning of the event or the end of the last asset)
  • INTERSTITIAL_ASSET_ENDED when streaming of an Interstitial asset has ended (before the next asset or the event ending)
  • INTERSTITIAL_ASSET_ERROR when an error occurs starting or streaming an Interstitial asset (this can include non-fatal errors such as stalling and errors that will end streaming of the asset, resulting in the schedule advancing to the next asset or fallback to primary)
  • INTERSTITIALS_PRIMARY_RESUMED when playback of primary content has begun or resumed from an Interstitial event
  • BUFFERED_TO_END when the last audio and video segments in the playlist have been appended (EOS signaled on all SourceBuffers)
  • AUDIO_TRACK_UPDATED similar to LEVEL_UPDATED fired for any update to audio group playlists
  • SUBTITLE_TRACK_UPDATED similar to LEVEL_UPDATED fired for any update to subtitle group playlists

Updated Events

  • MEDIA_ATTACHING, MEDIA_ATTACHED, MEDIA_DETACHING, and MEDIA_DETACHED include additional information (depending on whether media is being transferred)

New Errors for Interstitials

  • Type: NETWORK_ERROR
    • details: ASSET_LIST_LOAD_ERROR network error loading asset list
    • details: ASSET_LIST_LOAD_TIMEOUT network timeout error loading asset list
    • details: ASSET_LIST_PARSING_ERROR asset list was not valid JSON or missing required data
  • type: OTHER_ERROR details: INTERSTITIAL_ASSET_ITEM_ERROR an issue interrupted or prevented asset playback. This will result in skipping the remainder of the asset or falling back to primary content. The event error will contain more details. This type of error differs from the INTERSTITIAL_ASSET_ERROR events forwarded from asset player errors.

Build Constants

  • Interstitials is removed from the hls.light.js build by the __USE_INTERSTITALS__ build const/directive.

Known Issues

Should address before release (in order of priority):

  • Asset lists are not being loaded when enableInterstitialPlayback is set to false. They should be loaded. The schedule needs to reflect the path taken when primary playback is chosen over interstitial playback.
  • video element requires the autoplay or play() to be called on canplaythough after MediaSource reset from detach/attach for most schedule transitions (when appendInPlace is false). resolved with e1e515a (thanks to @matvp91 for calling this out)
  • InterstitialsManager primary, playout and integrated durations are non-finite for Live streams. These should be finite for easier mapping of controls. resolved with 5842cc2
  • hls.interstitialsManager.skip() does not skip Interstitial events that do not reset MediaSource (where InterstitialEvent appendInPlace is true) resolved with 0021cfd e84bc30 2c3c372
  • In some cases the MediaSource is reset at the end of a post roll or playback ends at the end of the post roll, but attempting to play from that state does not restart the program.

No plan to address before release:

  • There is no API provided for defining client-side Interstitials (file an issue stating your interest and use case).
  • Primary subtitles TextTrack cues are not edited to avoid overlapping with Interstitial events scheduled without MediaSource reset (where InterstitialEvent appendInPlace is true). Note that this only applies to Interstitials that overlap and replace primary segments completely which is not the case for most Interstitials. If you have content where this is a concern, please file an issue.
  • Option to enforce X-RESTRICT=JUMP by applying seekable range. Current enforcement is implemented on "seeking" and calls to seekTo that restrict schedule advancement.
  • Present abutting interstitials and their assets in a gapless mode. This will reduce the number of MediaSource resets when not in appendInPlace mode and allow for single element and timeline playback of breaks independent of the primary player,

Resolves issues:

Checklist

  • changes have been done against master branch, and PR does not conflict
  • new unit / functional tests have been added (whenever applicable)
  • API or design changes are documented in API.md

@matvp91
Copy link

matvp91 commented Sep 9, 2024

@robwalch, as discussed earlier, this is the case where only the first fragment is fetched / buffered when an interstitial is scheduled beyond the main timeline's duration: https://feature-interstitials.hls-js-4zn.pages.dev/demo/?src=https%3A%2F%2Fstitcher.mixwave.stream%2Fdirect%2F2eb5dfd5-7256-49f5-95c5-e86e1661e733%2Fmaster.m3u8%3Fparams%3DeyJpbnRlcnN0aXRpYWxzIjpbeyJ0aW1lT2Zmc2V0IjowLCJhc3NldElkIjoiNDZiYjRmZWYtNDQwYS01NTNhLWI1MTktMDg5ZWZmNDMwNmY2IiwidHlwZSI6ImFkIn0seyJ0aW1lT2Zmc2V0Ijo2MCwiYXNzZXRJZCI6IjQ2YmI0ZmVmLTQ0MGEtNTUzYS1iNTE5LTA4OWVmZjQzMDZmNiIsInR5cGUiOiJhZCJ9XX0%3D&demoConfig=eyJlbmFibGVTdHJlYW1pbmciOnRydWUsImF1dG9SZWNvdmVyRXJyb3IiOnRydWUsInN0b3BPblN0YWxsIjpmYWxzZSwiZHVtcGZNUDQiOmZhbHNlLCJsZXZlbENhcHBpbmciOi0xLCJsaW1pdE1ldHJpY3MiOi0xfQ==

The params payload is a base64 encoded string with the following info:

{
  "interstitials": [{
    "timeOffset": 0,
    "assetId": "46bb4fef-440a-553a-b519-089eff4306f6",
    "type": "ad"
  }, {
    "timeOffset": 60,
    "assetId": "46bb4fef-440a-553a-b519-089eff4306f6",
    "type": "ad"
  }]
}

When you exclude the timeOffset=60 (as it's the offset exceeding the main timeline duration), it'll continue buffering fragments normally.

robwalch and others added 18 commits September 9, 2024 15:27
…grated duration

(cherry picked from commit 161bf44d0186a263bc82299fc09e52b913f2d2f8)
…itialsManager.skip()`

(cherry picked from commit 928064edb0d802d1e6513bd509e65ef5c584f2e9)
(cherry picked from commit ffa6612c848033ea913c020e0e5355db710ff431)
(cherry picked from commit 1cc169ce64b26aff31929a510b8ff157558c9bb5)
(cherry picked from commit 34ab0ed5016a3c18e330ab136fd731c3d5c95fcc)
…nterstitials

Make criteria for "append in place" more flexible (does not require snapping and allows for resume offset matching duration)

(cherry picked from commit 856dfb09aeadc99c5f9788ca5e55310fd8fa4d3d)
(cherry picked from commit 446ef3cee40ab0f7337fe12c905905349f24d8f7)
…ce" mode

Improve Interstitial logging and log media transfer and "appendInPlace" details
Fix media transfer for Interstitials in "appendInPlace" mode, with abutting timing, and snap-in

(cherry picked from commit 9911bc4b8f9d59a4843affd7b889b11aab2c0117)
Fix asset player startPosition with append-in-place timelineOffset
Fix issues with item and asset lookup using Array.indexOf when object refererenes are replaced by Live updates
Fix cue removal by asset players

(cherry picked from commit eb44d8699e4fa48becdba377aa4a82ece6d1f6f3)
(cherry picked from commit c61f5e3f6c480212ea79ed68a511208fe8674b99)
(cherry picked from commit 1b843d5517a658cdbd20bbb5eb02a64bb0839291)
Add comments describing each field in `InterstitialsManager` API markdown

(cherry picked from commit bcb2db84d1ffba18befd658b284695fb0a69f7d4)
…outside the primary program time range

Addresses comment #6591 (comment)

(cherry picked from commit ebe6c08e22a779c90cba7cd8f257841898197e11)
@robwalch
Copy link
Collaborator Author

robwalch commented Sep 10, 2024

this is the case where only the first fragment is fetched / buffered when an interstitial is scheduled beyond the main timeline's duration

Thanks for the repro. I've pushed a fix to the scheduler that corrects an assumption that the previous event before the last primary segment must always be the last interstitial event. ef72aa0

        // last primary segment
        const timelineStart = primaryPosition;
        const integratedStart = integratedTime;
        const segmentDuration = primaryDuration - primaryPosition;
        integratedTime += segmentDuration;
        const playoutStart = playoutDuration;
        playoutDuration += segmentDuration;
        schedule.push({
-          previousEvent: interstitialEvents[interstitialEvents.length - 1],
+          previousEvent: schedule[schedule.length - 1].event || null,

That caused a lookup here (

const bufferingIndex = this.findItemIndex(bufferingItem);
) to return an invalid index and setting the incorrect buffering item and preventing primary segments from loading.

…layback state management)

(cherry picked from commit 3f25e0ffbb0e2b883875dfff5bd6d74669968800)
@matvp91
Copy link

matvp91 commented Sep 10, 2024

Checked it out and your change also fixed a bug where subtitle selection would only fetch the text playlist but not the fragments. See (

const bufferingItem = interstitials?.bufferingItem;
), it bailed out early, _loadFragForPlayback was never called. Although I was running the same stream with the interstitial offset exceeding the main timeline duration so I'm not sure how representable the bug was in general. Nonetheless, it's good to know.

Minor: hls.interstitialsManager.integrated.seekTo($0.duration) is ignored. From a UI perspective, a user could seek to the end of an asset.

Thought 1: let's say there's an interstitial at 10, hls.interstitialsManager.integrated.seekTo(15) seeks over it but the timeline is internally adjusted to trigger the interstitial. Would that imply manually adjusting interstitial.resumeOffset (X-RESUME-OFFSET)? As far as I know, there's no spec definition to indicate a dynamic resume offset (to match the last requested main timeline time).

Thought 2: interstitial.hasPlayed indicates that an interstitial has been consumed before, would it make sense to have an event to influence whether an interstitial can be ignored? I think this goes hand in hand with a client API to change interstitials and throw them out of the schedule once they've finished. Wdyt?

…after flush or reset

(cherry picked from commit 5a26e0067c44294e2e0724d9b01ac095a513acde)
@robwalch robwalch changed the title HLS Interstitials support HLS Interstitials Support Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants