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

v1.0.0 (LHLS Chunked Transfer Support, IMSC1, Refactor) #2370

Closed
wants to merge 237 commits into from

Conversation

johnBartos
Copy link
Collaborator

@johnBartos johnBartos commented Sep 13, 2019

v1.0.0

Overview

Earlier this year, the JW Player team set out to add low-latency streaming support to Hls.js. The focus of this work was around progressive streaming - the ability to stream a segment as a series of chunks, from fetch to buffer. To do so we had to refactor the core segment flow (load, transmux, buffer) so that it could handle streaming chunks instead of whole segments.

At a high level, we refactored the architecture to propagate data via return statements and direct callbacks, instead of through the Hls event bus. For example, fragment loading is now done through a promise returned by the fragment loader, instead of being triggered by FRAG_LOADING and received by FRAG_LOADED. We made changes similar to this throughout the pipeline.

We needed to make these changes because of an assumption built into the core of Hls.js - only one segment will be loading at any given time (per controller), and that segment will be only in one state (loading, parsing, or buffering). A lot of logic in Hls.js was built around this assumption. However, when progressively streaming, this isn't true - a segment can be both loading, transmuxing, and buffering at the same time. Unfortunately the event bus architecture made this very difficult to change, since it assumes that events received outside of their expected state are invalid. For example, Hls.js will throw away FRAG_PARSING_DATA events if the current state is not parsing. Direct data flow made it very easy to break these assumptions because we could effectively do away with using state to control fragment loading flow. This new architecture also has a side benefit where handlers are not receiving events for things they don't care about (for example, the audio-stream-controller handling all FRAG_LOADED events, even from ones it did not initiate).

Transmuxing data flow was refactored in a similar way - we moved away from events in favor of returns and directly-set callbacks. But more importantly, we refactored each demuxer and remuxer so that it can process segments as a series of chunks instead of a single, whole unit. Along the way we also made a lot of improvements to how we transmux, especially when it comes to CMAF/fmp4.

Our hypothesis for results is that progressive streaming will give us a faster time-to-first-frame, with a modest increase to the amount of stalls per play session. This is because when on a slow connection, you'll probably stall more often when buffering small chunks as opposed to whole segments. Over the past few months we've been doing A/B tests in production, and the results are in-line with our hypothesis. Approximately 10% more viewers have a TTFF of under 1 second compared to non-progressive, but those viewers also stall for longer than non-progressive viewers (95% stall for under 1 second non-progressive, while only 93% stall for under 1s with the progressive provider). Furthermore, we see that on old devices performance isn't as good as non-progressive streaming. We hypothesize that this is because of the additional overhead incurred when progressively streaming - some devices are too slow to take advantage of it.

On the JW side, we're continuing to iterate and improve performance. For example, we put in a change that enforces the minimum chunk size to be at least 16kb when streaming progressive. We think that this will help balance the scheduling overhead of chunks with the benefit of buffering faster. We're also continuing to work to determine which devices benefit most from progressive, so that we can intelligently set the progressive config flag.

This PR is not yet complete, but I think it's better to get this pushed up for reviewer sooner than later. I think there's a bug or two with this merge right now (see Known Issues below), but those will be worked out soon. Overall we're seeing that almost all of our streams are playing as expected - we'd love for you to test it too.

Changelist

Breaking Changes

  • Promise support is now required. Please bring your own polyfill
  • The stats object has changed. See below for more info
  • enableSoftwareAES will always be used if set to true
    • Set to true if progressive is true
  • On the Fragment object:
    • hasElementaryStream function has been removed
    • setElementaryStream function has been removed
    • _elementaryStreams property has been removed

General

  • By default (and where supported), Hls.js will progressively stream segments. This means that segments are loaded, transmuxed, and buffered in small (at least 16kb) chunks. This feature improves time to buffer segments, but requires more computation power.
  • New config option: progressive (boolean)
    • Sets the fetch-loader as the fragment and playlist loader, which enables progressive streaming
    • True by default, but will be toggled to false if not supported
    • If set false, will not be enabled even if supported
    • Can be dynamically enabled/disabled; takes effect from the next loaded segment
  • New config option: renderNatively (boolean, false by default)
    • Allows text tracks and cues to be received via Hls.js events instead of TextTracks and Cues being added to the MediaElement
  • New config option: bitrateTest (boolean)
    • Enables or disables Hls.js' startup logic to test client bandwidth
    • True by default; if false, the bitrate test will not be performed
  • Enhanced fragment stat recording, including per-chunk statistics
  • Lots of TypeScript additions and conversions
  • Console logs are prefixed with a class or module name

Fragment Loaders

  • The fragment-loader now returns a promise when loading is complete
  • When Hls.js is playing progressively, the onProgress is called whenever a chunk of data is available
    • The minimum chunk size is set by the highWaterMark option, which defaults to 16kb. We found this to be a good default size.
  • Hls.js events are no longer emitted
    • Compatibility is maintained in the base-stream-controler, which emits loading events with the same signature/payload
  • The fetch-loader always streams progressively when enabled; xhr-loader is used for non-progressive streaming

Stream Controllers

  • We unified logic between the stream-controller and audio-stream-controller. The bulk of segment logic is now found in the base-stream-controller
    • Most notably, both controllers now use the same function to determine the next segment to load. This greatly improves startup time when playing streams with alternate audio, as both controllers now load the correct segment given the current time.
  • Controllers directly own and instantiate their own fragment-loader
    • The FRAGMENT_LOADING event is no longer used to move the binary segment data from loader to controller; loading is now handled via promises
  • Controllers have separate code paths for handling segments for playback, the init segment, and segments to test bitrate (which are not played)
    • This allowed us to reduce the number of logic branches needed in segment handling functions
  • Controllers now register callbacks directly with the Transmuxer to receive transmuxed chunks
  • Simplified buffering logic
    • pendingBuffering and appended flags have been removed
    • Data is always immediately buffered when received from the transmuxer
    • No longer listens for the end of buffering; the buffer-controller now emits the FRAG_BUFFERED event

Audio Stream Controller

  • Removed initSegment caching logic (pendingData) when loading the init segment during audio track switch
    • The initSegment is now always loaded first, so we don't have to explicitly enforce it

Transmuxer

  • All supported media types can be progressively transmuxed
    • Including AES encrypted streams; SAMPLE-AES still needs testing (TODO)
  • demuxer has been renamed to transmuxer-interface
  • demuxer-worker has been renamed to transmuxer-worker
  • demuxer-inline has been renamed to transmuxer
  • The transmuxer now returns data directly, instead of triggering callbacks when transmuxing completes
    • Data is communicated to the controller via two callbacks:
      • handleTransmuxComplete is called whenever data has been transmuxed
      • handleTransmuxerFlush is called when the current segment has finished transmuxing
    • All transmuxed data (audio, video, text, id3, initSegment) are returned in the same object, and the same call when possible
    • Only the ERROR and FRAG_DECRYPTED events are emitted from the Transmuxer
  • Transmuxing now happens in two steps: push and flush
    • During progressive streaming, push is called for each chunk
    • flush is called when segment loading has completed
    • Each call can return one or more TransmuxResult objects, or a promise which resolves a TransmuxResult (in the case of AES)
  • Improved transmux handling around discontinuities, seeking, and rendition switching
  • Heavy refactoring (in TypeScript) to improve readability and maintainability
  • Improved support for CMAF/fmp4 media
    • Most notably, Hls.js now determines the duration of a segment from the media instead of the manifest

See jwplayer#199 for a more in-depth list of changes

Buffer Controller

  • The buffer-controller has been refactored to schedule buffer operations via queues
    • Can now buffer audio and video in parallel
    • Simplifies buffering logic (especially when considering the updating SourceBuffer flag)
    • Improves handling of tricky edgecases caused by race conditions
  • Uint8Array chunks (MOOF + MDAT) are combined before buffering in order to reduce the number of appends
  • Emits the FRAG_BUFFERED event when all chunks for the current segment have been buffered
    • No longer emitted by stream controllers
  • Heavily refactored to improve readability and maintainability

Stats

  • The stats object has been reworked in order to accurately capture stats while progressively streaming. Check out the new schema in load-stats.ts

See jwplayer#239 for more detail on the changes

Playlist Loading

  • The playlist loader now attempts to synchronize with playlist updates from the server. This results in less playlist reload misses.

Captions

  • 4 608/708 channels are now supported
  • Cues/tracks can now be emitted via Hls.js events instead of by adding to the text track (see renderNatively above)
    • This allows developers to provide their own captions rendering engine

LHLS

  • Support for playing PREFETCH segments (as defined in the open LHLS spec) has been added
    • There is no playback rate controller yet, so latency cannot be guaranteed

Copy link
Member

@tjenkinson tjenkinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes sound great!

only skimmed through them

package.json Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
src/controller/cap-level-controller.ts Outdated Show resolved Hide resolved
src/crypt/decrypter.ts Outdated Show resolved Hide resolved
src/demux/transmuxer-interface.ts Outdated Show resolved Hide resolved
src/polyfills/runtime-polyfills.ts Outdated Show resolved Hide resolved
yarn.lock Outdated Show resolved Hide resolved
@tchakabam
Copy link
Collaborator

Shouldn't the CI be green also? :)

@tjenkinson
Copy link
Member

I emailed netlify to get more info on why the demo build/deploy is failing. Wondering if it's hitting a memory limit

@tjenkinson
Copy link
Member

@robwalch npm run docs is broken, and this is also why the netlify build is failing. The end of the logs are chopped off because netlify is terminating before it gets chance to upload them all.

$ npm run docs

> hls.js@ docs /Users/tomjenkinson/Documents/GitHub/hls.js
> esdoc

parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/.external-ecmascript.js
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/config.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/abr-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/audio-stream-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/audio-track-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/base-stream-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/buffer-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/buffer-operation-queue.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/cap-level-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/eme-controller.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/fps-controller.js
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/fragment-finders.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/fragment-tracker.ts
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/gap-controller.js
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/id3-track-controller.js
parse: /Users/tomjenkinson/Documents/GitHub/hls.js/src/controller/level-controller.ts
{ SyntaxError: Unexpected token (213:52)
    at Parser.pp$5.raise (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:4454:13)
    at Parser.pp.unexpected (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:1761:8)
    at Parser.pp$3.parseExprAtom (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3750:12)
    at Parser.pp$3.parseExprSubscripts (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3494:19)
    at Parser.pp$3.parseMaybeUnary (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3474:19)
    at Parser.pp$3.parseExprOps (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3404:19)
    at Parser.pp$3.parseMaybeConditional (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3381:19)
    at Parser.pp$3.parseMaybeAssign (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3344:19)
    at Parser.pp$3.parseExprListItem (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:4312:16)
    at Parser.pp$3.parseCallExpressionArguments (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/babylon/lib/index.js:3573:20) pos: 6471, loc: Position { line: 213, column: 52 } }
/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/Factory/DocFactory.js:135
    for (const exportNode of this._ast.program.body) {
                                       ^

TypeError: Cannot read property 'program' of undefined
    at DocFactory._inspectExportDefaultDeclaration (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/Factory/DocFactory.js:135:40)
    at new DocFactory (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/Factory/DocFactory.js:90:10)
    at Function._traverse (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDoc.js:233:21)
    at _walk.filePath (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDoc.js:112:25)
    at Function._walk (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDoc.js:204:9)
    at Function._walk (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDoc.js:206:14)
    at Function.generate (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDoc.js:96:10)
    at ESDocCLI.exec (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDocCLI.js:71:23)
    at Object.<anonymous> (/Users/tomjenkinson/Documents/GitHub/hls.js/node_modules/esdoc/out/src/ESDocCLI.js:182:7)
    at Module._compile (internal/modules/cjs/loader.js:805:30)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! hls.js@ docs: `esdoc`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the hls.js@ docs script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/tomjenkinson/.npm/_logs/2019-09-18T20_33_34_340Z-debug.log

@robwalch
Copy link
Collaborator

robwalch commented Sep 19, 2019

@tjenkinson I narrowed down the eslint issue to both level-controller and level-helper. I'll continue the process of elimination by deleting sections of code and comments until I find the culprit.

Here's one. I guess it doesn't like inline typing.

<ManifestParsedData>{
        levels,
        audioTracks,
        firstLevel: this._firstLevel,
        stats: data.stats,
        audio: audioCodecFound,
        video: videoCodecFound,
        altAudio: audioTracks.some(t => !!t.url)
      }

@robwalch
Copy link
Collaborator

Fixed the docs task locally, but netlify is still failing. Would that task really be handled during the "preparing repo" stage?

11:36:34 AM: failed during stage 'preparing repo': exit status 1

@tjenkinson
Copy link
Member

@robwalch no that’s unrelated. Seen it happen a few times now :/

If it happens again I’ll email them about that too. I restarted it and it worked

@tjenkinson
Copy link
Member

If it happens again I’ll email them about that too. I restarted it and it worked

Got a reply back about this. It was a known issue with the image we were using. I updated it to the latest one and it should be fixed now 🤞

@robwalch
Copy link
Collaborator

robwalch commented Sep 25, 2019

See #2370 (comment)

scripts/precommit.sh Outdated Show resolved Hide resolved
Copy link
Member

@michaelcunningham19 michaelcunningham19 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, @johnBartos @robwalch ! 🥇

I scanned through the entire diff and focused on the areas which mattered (the progressive bits) as many of the changed files only have minor, unrelated changes (e.g. let ->const).

I haven't tested out this build yet, but I plan on doing so in the near future and will report back on my experiences

Cheers 🍻

src/controller/buffer-controller.ts Outdated Show resolved Hide resolved
src/controller/buffer-controller.ts Outdated Show resolved Hide resolved
// in case any error occured while appending, put back segment in segments table
logger.error(`[buffer-controller]: Error encountered while trying to append to the ${type} SourceBuffer`, err);
const event = { type: ErrorTypes.MEDIA_ERROR, parent: frag.type, details: '', fatal: false };
if (err.code === 22) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, would be nice to have an enum with the possible codes added, for readability purposes.

Copy link
Collaborator

@robwalch robwalch Oct 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not finding any good sources for error documentation. So I would also guess that vendors have implemented these errors differently across browsers.

One that I can reproduce in Chrome (I called load() on the video after clearing the source) with code 11 is Error encountered while trying to append to the video SourceBuffer DOMException: Failed to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer has been removed from the parent media source.

We shouldn't retry in that case but it's no BUFFER_FULL_ERROR. I'd love to improve the error handling here. The biggest thing missing IMO is an error or err property on the event with the source Error object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robwalch - Good point on the lack of good documentation, I couldn't find much else either. All I found was: https://www.w3.org/TR/WebIDL-1/#quotaexceedederror

I fiddled with this a bit and found out that this is part of the TypeScript library out-of-box:

let x = DOMException.QUOTA_EXCEEDED_ERR;  // number

See screenshot of it in action in the browser (Google Chrome 77):
Screen Shot 2019-10-07 at 8 28 35 AM

Copy link
Collaborator

@robwalch robwalch Oct 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool. Support is good too. Thanks @michaelcunningham19! I made the change to check:
if (err.code === DOMException.QUOTA_EXCEEDED_ERR) {

src/controller/buffer-controller.ts Outdated Show resolved Hide resolved
src/controller/buffer-controller.ts Outdated Show resolved Hide resolved
src/hls.ts Outdated Show resolved Hide resolved
src/polyfills/runtime-polyfills.ts Outdated Show resolved Hide resolved
src/types/bufferAppendingEventPayload.ts Outdated Show resolved Hide resolved
src/types/general.ts Outdated Show resolved Hide resolved

public transmuxing: HlsChunkPerformanceTiming = { start: 0, executeStart: 0, executeEnd: 0, end: 0 };
public buffering: { [key in SourceBufferName]: HlsChunkPerformanceTiming } = {
audio: { start: 0, executeStart: 0, executeEnd: 0, end: 0 },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could have a named type for this for audio, video, and audiovideo

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's HlsChunkPerformanceTiming. Am I missing something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I mis-read this initially. I thought this was an inline type definition. The type definition is correct.

My first comment was about the multiple inline usage of implementing those timing types for the initial values ({ start: 0, executeStart: 0, executeEnd: 0, end: 0 })

Thoughts on making a helper for this?
e.g.

function getNewPerformanceTiming(): HlsChunkPerformanceTiming {
  return { start: 0, executeStart: 0, executeEnd: 0, end: 0 }
}

const timing = getNewPerformanceTiming()

Having this helper may make future refactoring easier

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Rob Walch and others added 23 commits September 22, 2020 19:20
… when unnecessary or past attempts failed
- Mark LL-HLS test streams as live
- Comment out unavailable test stream
- Comment out HEVC test stream (functional tests expect streams that contain codec variants playable by Chrome desktop)
- Do not adjustSliding twice with delta playlist
- Add LevelDetail adjustedSliding distinguished from PTSKnown (+5 squashed commits)
- Align streams without DISCONTINUITY, PROGRAM-DATE-TIME, or known-PTS on SEQUENCE-NUMBER to improve first fragment requested
- Fix delta playlist merging without fragment PTS (adjustSliding with skipped segments)
- Fix playlist sliding check
- Sync levels on PTS by setting transmuxer defaultInitPTS
Add playbackRate controls to demo
- Add lowLatencyMode config (doesn't yet disable part playlist loading)
- Add SKIP delta playlist tag to README
Cleanup live backbuffer removal
Cleanup fragment-tracker eviction from buffer flushes
Load main fragment at `this.nextLoadPosition` when loading candidate is already loaded
* upstream_hls.js/master:
  Minor clarification of exactly what occurs at initialLiveManifestSize.
  Update API.md
  Bump sinon from 9.0.3 to 9.1.0
  Bump eslint-plugin-import from 2.22.0 to 2.22.1
  Bump @types/sinon-chai from 3.2.4 to 3.2.5
  Bump karma from 5.2.2 to 5.2.3
  Bump netlify-cli from 2.64.0 to 2.64.1
  Bump netlify-cli from 2.63.3 to 2.64.0

# Conflicts:
#	package-lock.json
@robwalch
Copy link
Collaborator

robwalch commented Oct 5, 2020

This PR has been replaced by #3072

@video-dev video-dev locked and limited conversation to collaborators Oct 5, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants