Home Reference Source

src/loader/key-loader.ts

/*
 * Decrypt key Loader
 */
import { ErrorTypes, ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import {
  LoaderStats,
  LoaderResponse,
  LoaderConfiguration,
  LoaderCallbacks,
  Loader,
  KeyLoaderContext,
} from '../types/loader';
import { LoadError } from './fragment-loader';
import type { HlsConfig } from '../hls';
import type { Fragment } from '../loader/fragment';
import type { ComponentAPI } from '../types/component-api';
import type { KeyLoadedData } from '../types/events';

export default class KeyLoader implements ComponentAPI {
  private readonly config: HlsConfig;
  public loader: Loader<KeyLoaderContext> | null = null;
  public decryptkey: Uint8Array | null = null;
  public decrypturl: string | null = null;

  constructor(config: HlsConfig) {
    this.config = config;
  }

  abort(): void {
    this.loader?.abort();
  }

  destroy(): void {
    if (this.loader) {
      this.loader.destroy();
      this.loader = null;
    }
  }

  load(frag: Fragment): Promise<KeyLoadedData | void> | never {
    const type = frag.type;
    const loader = this.loader;
    if (!frag.decryptdata) {
      throw new Error('Missing decryption data on fragment in onKeyLoading');
    }

    // Load the key if the uri is different from previous one, or if the decrypt key has not yet been retrieved
    const uri = frag.decryptdata.uri;
    if (uri !== this.decrypturl || this.decryptkey === null) {
      const config = this.config;
      if (loader) {
        logger.warn(`abort previous key loader for type:${type}`);
        loader.abort();
      }
      if (!uri) {
        throw new Error('key uri is falsy');
      }
      const Loader = config.loader;
      const keyLoader =
        (frag.keyLoader =
        this.loader =
          new Loader(config) as Loader<KeyLoaderContext>);
      this.decrypturl = uri;
      this.decryptkey = null;

      return new Promise((resolve, reject) => {
        const loaderContext: KeyLoaderContext = {
          url: uri,
          frag: frag,
          part: null,
          responseType: 'arraybuffer',
        };

        // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times,
        // key-loader will trigger an error and rely on stream-controller to handle retry logic.
        // this will also align retry logic with fragment-loader
        const loaderConfig: LoaderConfiguration = {
          timeout: config.fragLoadingTimeOut,
          maxRetry: 0,
          retryDelay: config.fragLoadingRetryDelay,
          maxRetryDelay: config.fragLoadingMaxRetryTimeout,
          highWaterMark: 0,
        };

        const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
          onSuccess: (
            response: LoaderResponse,
            stats: LoaderStats,
            context: KeyLoaderContext,
            networkDetails: any
          ) => {
            const frag = context.frag;
            if (!frag.decryptdata) {
              logger.error('after key load, decryptdata unset');
              return reject(
                new LoadError({
                  type: ErrorTypes.NETWORK_ERROR,
                  details: ErrorDetails.KEY_LOAD_ERROR,
                  fatal: false,
                  frag,
                  networkDetails,
                })
              );
            }
            this.decryptkey = frag.decryptdata.key = new Uint8Array(
              response.data as ArrayBuffer
            );

            // detach fragment key loader on load success
            frag.keyLoader = null;
            this.loader = null;
            resolve({ frag });
          },

          onError: (
            error: { code: number; text: string },
            context: KeyLoaderContext,
            networkDetails: any
          ) => {
            this.resetLoader(context.frag, keyLoader);
            reject(
              new LoadError({
                type: ErrorTypes.NETWORK_ERROR,
                details: ErrorDetails.KEY_LOAD_ERROR,
                fatal: false,
                frag,
                networkDetails,
              })
            );
          },

          onTimeout: (
            stats: LoaderStats,
            context: KeyLoaderContext,
            networkDetails: any
          ) => {
            this.resetLoader(context.frag, keyLoader);
            reject(
              new LoadError({
                type: ErrorTypes.NETWORK_ERROR,
                details: ErrorDetails.KEY_LOAD_TIMEOUT,
                fatal: false,
                frag,
                networkDetails,
              })
            );
          },

          onAbort: (
            stats: LoaderStats,
            context: KeyLoaderContext,
            networkDetails: any
          ) => {
            this.resetLoader(context.frag, keyLoader);
            reject(
              new LoadError({
                type: ErrorTypes.NETWORK_ERROR,
                details: ErrorDetails.INTERNAL_ABORTED,
                fatal: false,
                frag,
                networkDetails,
              })
            );
          },
        };

        keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
      });
    } else if (this.decryptkey) {
      // Return the key if it's already been loaded
      frag.decryptdata.key = this.decryptkey;
      return Promise.resolve({ frag });
    }
    return Promise.resolve();
  }

  private resetLoader(frag: Fragment, loader: Loader<KeyLoaderContext>) {
    if (this.loader === loader) {
      this.loader = null;
    }
    loader.destroy();
  }
}