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

Refactor fragment loading to be promise-based & Typescript loaders #2123

Closed
wants to merge 6 commits into from

Conversation

johnBartos
Copy link
Collaborator

@johnBartos johnBartos commented Feb 9, 2019

This PR will...

  • Refactor fragment-loader.js:
    • No longer listens or emits events
    • load now takes a fragment and returns a promise
      • Resolves replaces the FRAG_LOADED event, and returns onSuccess
      • Reject replaces ERROR , and returns onError, and onTimeout
      • Removes loadprogress (for now)
      • Simplify fragment cancellation check (can check fragCurrent by reference)
  • Refactor *-stream-controller.js to use new promise based data flow
    • Each controller has a local fragment-loader instance
    • Calls fragmentLoader.load
    • Emits FRAG_LOADING for API compatibility
    • No longer listens to FRAG_LOADED
    • Removes logic branches handling different fragment loading scenarios (playback, bitrate test, and init segment)
    • Fires ERROR events on load error
  • _loadFragForPlayback:
    • Handles loading a frag which will be buffered
    • Calls the onFragLoaded function
    • Promise resolves to call
    • FiresFRAG_LOADED event for API compatibility
  • _loadBitrateTestFrag
    • Handles loading a frag for testing client bandwidth
    • Does not (!) emit FRAG_LOADED
  • _loadInitSegment
    • Handles loading the init segment
    • Does not (!) emit FRAG_LOADED
  • Typescript fragment-loader and xhr-loader
  • Add tests

This is a big one but each task is in its own commit:

The only catch: Hls.js now relies on Promises to be present. This is probably API breaking. We can merge this into a 1.0.0 branch if we decide not to ship this in a minor.

Why is this Pull Request needed?

Making fragment loading private and explicit to the controller which requested it allows us to simplify load callbacks. Instead of having each FRAG_LOADED callback check whether it should handle the event (by checking fragment type & controller state), promises allow each controller to handle only the fragment it requested. This allows us to remove a few checks at the top of each callback. Furthermore, we can split out specialized fragment loading scenarios (bitrate test, init segment) which are not demuxed into their own functions by attaching a different .then.

In addition to code cleanliness, this new pattern is the first step in enabling LHLS. LHLS requires progressive playback, which means that we're not going to have the same event flow that we have now; a fragment will be loading, parsing, and buffering at the same time, several times during its download. Our current architecture cannot handle this because it relies on events being fired in a specific sequence to propagate fragment data. By shifting to promises we eliminate the dependency on events for propagating fragment data.

Are there any points in the code the reviewer needs to double check?

  • Are there any places in which API compatibility has been broken
  • Any Typescript critiques? This is my first TS PR

Resolves issues:

N/A

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

@johnBartos
Copy link
Collaborator Author

@itsjamie

@itsjamie
Copy link
Collaborator

itsjamie commented Feb 10, 2019

@johnBartos
Copy link
Collaborator Author

johnBartos commented Feb 10, 2019 via email

Copy link
Collaborator

@itsjamie itsjamie left a comment

Choose a reason for hiding this comment

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

👍 on the typescript.

this.config = config;
}

load (frag: Fragment): Promise<FragLoadSuccessResult | FragLoadFailResult> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

There doesn't seem to be a method to define the type for the rejection case of a promise. I'm not sure what the best experience here would be, going to look at other public facing APIs for TypeScript.

What were you're experiences with typing load this way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I haven't tested this in another TS file, the controllers are still JS. I added this for TS completeness but I can remove it for now if it's too much of a hassle


return new Promise((resolve, reject) => {
if (!loader) {
return Promise.reject(new LoadError(null, 'Loader was destroyed after fragment request'));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of returning a new rejected promise, I think calling the reject callback here, or throwing would be better.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah right meant to do that

aborted: boolean
}

export interface LoaderCallbacks {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here is some typing code that I had locally when working on the playlist loader. The only real change is that it makes the Loader interface able to take concrete implementations of the loading context, and that the callbacks are typed because of that.

This will allow us to share the same Loader interface between Frag and Playlist loaders without having a bunch of optional properties. For the Typescript side, it uses the generics part of the language to enable this.

export interface LoaderContext {
  // target URL
  url: string
  // loader response type (arraybuffer or default response type for playlist)
  responseType: XMLHttpRequestResponseType
  // start byte range offset
  rangeStart?: number
  // end byte range offset
  rangeEnd?: number
  // true if onProgress should report partial chunk of loaded content
  progressData?: boolean
}

export interface LoaderResponse {
  url: string,
  data: string | ArrayBuffer
}

export interface LoaderConfiguration {
  // Max number of load retries
  maxRetry: number
  // Timeout after which `onTimeOut` callback will be triggered
  // (if loading is still not finished after that delay)
  timeout: number
  // Delay between an I/O error and following connection retry (ms).
  // This to avoid spamming the server
  retryDelay: number
  // max connection retry delay (ms)
  maxRetryDelay: number
}

type LoaderOnSuccess < T extends LoaderContext > = (
  response: LoaderResponse,
  stats: LoaderStats,
  context: T,
  networkDetails: any
) => void;

type LoaderOnProgress < T extends LoaderContext > = (
  stats: LoaderStats,
  context: T,
  data: string | ArrayBuffer,
  networkDetails: any,
) => void;

type LoaderOnError < T extends LoaderContext > = (
  error: {
    // error status code
    code: number,
    // error description
    text: string,
  },
  context: T,
  networkDetails: any,
) => void;

type LoaderOnTimeout < T extends LoaderContext > = (
  stats: LoaderStats,
  context: T,
) => void;

export interface LoaderCallbacks<T extends LoaderContext>{
  onSuccess: LoaderOnSuccess<T>,
  onError: LoaderOnError<T>,
  onTimeout: LoaderOnTimeout<T>,
  onProgress?: LoaderOnProgress<T>,
}

export interface Loader<T extends LoaderContext> {
  destory(): void
  abort(): void
  load(
    context: LoaderContext,
    config: LoaderConfiguration,
    callbacks: LoaderCallbacks<T>,
  ): void

  context: T
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice I'll add these in

@johnBartos johnBartos changed the base branch from master to 1.0.0 February 10, 2019 23:33
@johnBartos
Copy link
Collaborator Author

I changed the base branch from master to 1.0.0 so we don't have to worry about maintaining API compatibility in this PR.

@itsjamie I noticed some weird results running tests locally with Istanbul enabled. Have you noticed that it's not always running all tests? On one run it'll say something like 276 assertions and on the next it'll be 300 something.

@itsjamie
Copy link
Collaborator

Yeah, I've noticed Istanbul doing weird things reporting different test numbers.

@johnBartos
Copy link
Collaborator Author

@itsjamie Addressed feedback & integrated your types from the playlist-loader TS pr

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.

LGTM 👍

✅ All demo page assets work as expected (with the exception of a couple streams which are unavailable)
✅ Can't reproduce the issue spotted by @itsjamie when checking out this branch locally and testing. But I can see it on the deploy preview (updates to this branch might not have been propagated to netlify?)

@johnBartos @itsjamie
I think it would be good to get this in the 1.0.0 branch soon as it's a pre-req for LHLS

@@ -124,7 +124,7 @@ export default class Hls extends Observer {
const capLevelController = new config.capLevelController(this);
const fpsController = new config.fpsController(this);
const playListLoader = new PlaylistLoader(this);
const fragmentLoader = new FragmentLoader(this);
// const fragmentLoader = new FragmentLoader(this);
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if you wanna remove or keep this @johnBartos ?

@johnBartos
Copy link
Collaborator Author

No longer valid, I've made a bunch of changes since opening this PR. Would probably be easier to bundle this with all of the other refactoring work I've done for 1.0.0

@johnBartos johnBartos closed this May 8, 2019
@robwalch robwalch deleted the upstream/chore/frag-loading-refactor branch March 15, 2020 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants