import type {
  ISourceBufferListEventMap,
  IMediaSource,
  IMediaSourceEventMap,
  ISourceBuffer,
  ISourceBufferEventMap,
  ISourceBufferList,
} from "../../../compat/browser_compatibility_types";
import {
  extractInitSegment,
  findCompleteBox,
  getDurationFromTrun,
  getMDHDTimescale,
  getTrackFragmentDecodeTime,
  extractCompleteChunks,
} from "../../../parsers/containers/isobmff";
import { getKeyIdFromInitSegment } from "../../../parsers/containers/isobmff/utils";
import arrayIncludes from "../../../utils/array_includes";
import EventEmitter from "../../../utils/event_emitter";
import noop from "../../../utils/noop";
import type { IRange } from "../../../utils/ranges";
import startsWith from "../../../utils/starts_with";
import { bytesToHex } from "../../../utils/string_parsing";
import TaskCanceller from "../../../utils/task_canceller";
import TimeRangesWithMetadata, { EventScheduler } from "./utils";

/**
 * Re-implementation of the MSE `MediaSource` Object.
 * @class DummyMediaSource
 */
export class DummyMediaSource
  extends EventEmitter<IMediaSourceEventMap>
  implements IMediaSource
{
  public readonly isDummy: true;
  public handle: DummyMediaSource;
  public sourceBuffers: IDummySourceBufferList;
  public activeSourceBuffers: IDummySourceBufferList;
  public readyState: "closed" | "open" | "ended";
  public destroyed: boolean;
  private _duration: number;
  public liveSeekableRange: TimeRangesWithMetadata<null>;

  public onsourceopen: ((evt: Event) => void) | null;
  public onsourceended: ((evt: Event) => void) | null;
  public onsourceclose: ((evt: Event) => void) | null;

  public eventScheduler: EventScheduler;
  private _callbacks: IDummyMediaSourceCallbacks;

  constructor() {
    super();
    this.isDummy = true;
    this.sourceBuffers = createDummySourceBufferList();
    this.activeSourceBuffers = createDummySourceBufferList();
    this.readyState = "closed";
    this.handle = this;
    this.onsourceopen = null;
    this.onsourceended = null;
    this.onsourceclose = null;
    this.destroyed = false;
    this._callbacks = {
      hasMediaElementErrored: () => false,
      onBufferedUpdate: noop,
      updateMediaElementDuration: noop,
    };
    this._duration = NaN;
    this.liveSeekableRange = new TimeRangesWithMetadata();
    this.eventScheduler = new EventScheduler();
  }

  static isTypeSupported(mimeType: string): boolean {
    return startsWith(mimeType, "audio/mp4") || startsWith(mimeType, "video/mp4");
  }

  public get duration(): number {
    if (this.readyState === "closed") {
      return NaN;
    }
    return this._duration;
  }

  public set duration(givenDuration: number) {
    const duration = Number(givenDuration);
    if (isNaN(duration) || duration < 0) {
      throw new TypeError("Invalid duration");
    }
    if (this.readyState !== "open") {
      const err = new Error("`duration` updated on a non-open DummyMediaSource");
      err.name = "InvalidStateError";
      throw err;
    }
    if (this.sourceBuffers.some((s) => s.updating)) {
      const err = new Error("`duration` updated on an updating DummyMediaSource");
      err.name = "InvalidStateError";
      throw err;
    }

    if (duration === this.duration) {
      return;
    }

    const highestBuffered = this.sourceBuffers.reduce((acc, sb) => {
      if (sb.buffered.length === 0) {
        return acc;
      }
      return Math.max(sb.buffered.end(sb.buffered.length - 1), acc);
    }, 0);

    if (duration < highestBuffered) {
      const err = new Error("`duration` update lower than latest buffered coded frame");
      err.name = "InvalidStateError";
      throw err;
    }

    this._duration = duration;

    this._callbacks.updateMediaElementDuration(duration);
  }

  public addSourceBuffer(givenType: string): ISourceBuffer {
    const type = String(givenType);
    if (type === "") {
      throw new TypeError("`addSourceBuffer` error: empty string");
    }
    if (this.readyState !== "open") {
      const err = new Error("`addSourceBuffer` called on a non-open DummyMediaSource");
      err.name = "InvalidStateError";
      throw err;
    }
    const sb = new DummySourceBuffer({
      hasMediaElementErrored: () => {
        return this._callbacks.hasMediaElementErrored();
      },
      getMediaSourceDuration: () => {
        return this.duration;
      },
      getMediaSourceReadyState: () => {
        return this.readyState;
      },
      openMediaSource: () => {
        this.readyState = "open";
        this.eventScheduler.schedule(this, "sourceopen", null).catch(noop);
      },
      onBufferedUpdate: () => {
        this._callbacks.onBufferedUpdate();
      },
    });
    this.sourceBuffers.push(sb);
    this.sourceBuffers._onAddSourceBuffer();
    this.activeSourceBuffers.push(sb);
    this.activeSourceBuffers._onAddSourceBuffer();
    return sb;
  }

  public removeSourceBuffer(sb: ISourceBuffer): void {
    if (!arrayIncludes(this.sourceBuffers, sb)) {
      const err = new Error("`removeSourceBuffer` called on an unknown SourceBuffer");
      err.name = "NotFoundError";
      throw err;
    }

    if (sb.updating) {
      sb.updating = false;
      if (sb instanceof DummySourceBuffer) {
        if (sb.currentAppendCanceller !== null) {
          sb.currentAppendCanceller.cancel();
          sb.currentAppendCanceller = null;
          sb.eventScheduler
            .schedule(sb, "abort", null)
            .then(() => sb.eventScheduler.schedule(sb, "updateend", null))
            .catch(noop);
        }
        sb.canceller.cancel();
        sb.removed = true;
      }
    }

    if (sb instanceof DummySourceBuffer) {
      const indexOfActive = this.activeSourceBuffers.indexOf(sb);
      if (indexOfActive >= 0) {
        this.activeSourceBuffers.splice(indexOfActive, 1);
        this.activeSourceBuffers._onRemoveSourceBuffer();
      }

      const index = this.sourceBuffers.indexOf(sb);
      if (index >= 0) {
        this.sourceBuffers.splice(index, 1);
        this.sourceBuffers._onRemoveSourceBuffer();
      }
    }
  }

  public destroy(): void {
    this.readyState = "closed";
    this._duration = NaN;
    this.activeSourceBuffers.forEach((sb) => this.removeSourceBuffer(sb));
    this.sourceBuffers.forEach((sb) => this.removeSourceBuffer(sb));
    this.destroyed = true;
    this.eventScheduler.schedule(this, "sourceclose", null).catch(noop);
  }

  public endOfStream(): void {
    if (this.readyState !== "open") {
      const err = new Error("`endOfStream` called on a non-open DummyMediaSource");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this.sourceBuffers.some((s) => s.updating)) {
      const err = new Error("`endOfStream` called on an updating DummyMediaSource");
      err.name = "InvalidStateError";
      throw err;
    }

    const end = this.sourceBuffers.reduce((acc, sb) => {
      if (sb.buffered.length === 0) {
        return acc;
      }
      const lastPos = sb.buffered.end(sb.buffered.length - 1);
      return Math.max(lastPos, acc);
    }, 0);
    this.duration = end;
    this.readyState = "ended";
    this.eventScheduler.schedule(this, "sourceended", null).catch(noop);
  }

  public updateCallbacks(cb: IDummyMediaSourceCallbacks): void {
    this._callbacks = cb;
  }

  public setLiveSeekableRange(start: number, end: number): void {
    if (this.readyState !== "open") {
      const err = new Error(
        "`setLiveSeekableRange` called on a non-open DummyMediaSource",
      );
      err.name = "InvalidStateError";
      throw err;
    }
    if (start < 0 || start > end) {
      const err = new Error("Invalid arguments given to `setLiveSeekableRange`");
      err.name = "InvalidStateError";
      throw err;
    }
    this.liveSeekableRange = new TimeRangesWithMetadata();
    this.liveSeekableRange.insert(start, end, null);
  }

  public clearLiveSeekableRange(): void {
    if (this.readyState !== "open") {
      const err = new Error(
        "`setLiveSeekableRange` called on a non-open DummyMediaSource",
      );
      err.name = "InvalidStateError";
      throw err;
    }
    this.liveSeekableRange = new TimeRangesWithMetadata();
  }
}

interface IDummySourceBufferInternalCallbacks {
  hasMediaElementErrored: () => boolean;
  getMediaSourceDuration: () => number;
  getMediaSourceReadyState: () => "open" | "ended" | "closed";
  openMediaSource: () => void;
  onBufferedUpdate: () => void;
}

/**
 * Re-implementation of the MSE `SourceBuffer` Object.
 * @class DummySourceBuffer
 */
export class DummySourceBuffer
  extends EventEmitter<ISourceBufferEventMap>
  implements ISourceBuffer
{
  /** Correspond to `SourceBuffer.prototype.updating` from MSE */
  public updating: boolean;

  /**
   * Set to `true` once this `DummySourceBuffer` is removed from the parent
   * MediaSource.
   */
  public removed: boolean;

  /**
   * When it emits, cancel all operations and scheduled events from this
   * `DummySourceBuffer`.
   */
  public canceller: TaskCanceller;

  /**
   * When it emits, abort specifically the current `appendBuffer` operation.
   */
  public currentAppendCanceller: TaskCanceller | null;

  /**
   * Allows to emit SourceBuffer events, simulating the browser's event loop.
   */
  public eventScheduler: EventScheduler;

  /**
   * To set to `true` to trigger `QuotaExceededError` to next `appendBuffer`
   * calls.
   * To set to `false` to stop doing that.
   */
  public BUFFER_FULL: boolean;

  /** Optional listener for the `updatestart` event. */
  public onupdatestart: ((evt: Event) => void) | null;
  /** Optional listener for the `update` event. */
  public onupdate: ((evt: Event) => void) | null;
  /** Optional listener for the `updateend` event. */
  public onupdateend: ((evt: Event) => void) | null;
  /** Optional listener for the `error` event. */
  public onerror: ((evt: Event) => void) | null;
  /** Optional listener for the `abort` event. */
  public onabort: ((evt: Event) => void) | null;

  /**
   * Set to `true` as soon as a segment with metadata is pushed.
   * Allows to indicate to the HTML5 implementation that it can reach the
   * `HAVE_METADATA` readyState.
   */
  public hasMetadata: boolean;

  /**
   * When set to an `Uint8Array`, set to the key id found in the last
   * initialization segment,
   * meaning next pushed media segments are probably encrypted with the
   * corresponding key.
   * When set to `null`, no initialization segment has been pushed or no key id
   * has been found on the last pushed initialization segment.
   */
  private _lastKeyId: Uint8Array | null;
  /**
   * When set to a `number`, set to the timescale found in the last
   * initialization segment that may be re-used for media segments.
   * When set to `null`, no initialization segment has been pushed or no
   * timescale has been found on the last pushed initialization segment.
   */
  private _lastInitTimescale: number | null;

  /** Correspond to `SourceBuffer.prototype.timestampOffset` from MSE */
  private _timestampOffset: number;
  /** Correspond to `SourceBuffer.prototype.buffered` from MSE */
  private _buffered: TimeRangesWithMetadata<IDummySourceBufferBufferMetadata>;
  /** Correspond to `SourceBuffer.prototype.appendWindowStart` from MSE */
  private _appendWindowStart: number;
  /** Correspond to `SourceBuffer.prototype.appendWindowEnd` from MSE */
  private _appendWindowEnd: number;
  /** Set to `true` if a "remove" operation is currently pending. */
  private _isRemoving: boolean;
  /** Callbacks given to a `DummySourceBuffer`. */
  private _callbacks: IDummySourceBufferInternalCallbacks;

  /**
   * @param {Object} callbacks
   */
  constructor(callbacks: IDummySourceBufferInternalCallbacks) {
    super();
    this.updating = false;
    this.removed = false;
    this.BUFFER_FULL = false;

    this.onupdatestart = null;
    this.onupdate = null;
    this.onupdateend = null;
    this.onerror = null;
    this.onabort = null;
    this.hasMetadata = false;
    this.canceller = new TaskCanceller();
    this.currentAppendCanceller = null;
    this._lastKeyId = null;
    this._callbacks = callbacks;
    this._buffered = new TimeRangesWithMetadata();
    this._appendWindowStart = 0;
    this._appendWindowEnd = Infinity;
    this._timestampOffset = 0;
    this._isRemoving = false;
    this._lastInitTimescale = null;
    this.eventScheduler = new EventScheduler();
  }

  /**
   * Implements `SourceBuffer.prototype.mode` from MSE.
   */
  public set mode(mode: "segments" | "sequence" | "dummy") {
    this._checkProp("mode", true);
    if (mode === "sequence") {
      throw new Error("Trying to update mode of DummySourceBuffer");
    }
  }
  public get mode(): "dummy" {
    return "dummy";
  }

  /**
   * Implements `SourceBuffer.prototype.timestampOffset` from MSE.
   */
  public set timestampOffset(timestampOffset: number) {
    this._checkProp("timestampOffset", true);
    this._timestampOffset = timestampOffset;
  }
  public get timestampOffset(): number {
    return this._timestampOffset;
  }

  /**
   * Implements `SourceBuffer.prototype.appendWindowStart` from MSE.
   */
  public set appendWindowStart(appendWindowStart: number) {
    this._checkProp("appendWindowStart", false);
    if (appendWindowStart < 0 || appendWindowStart >= this._appendWindowEnd) {
      const err = new TypeError("Invalid `appendWindowStart` set");
      throw err;
    }
    this._appendWindowStart = appendWindowStart;
  }

  public get appendWindowStart(): number {
    return this._appendWindowStart;
  }

  /**
   * Implements `SourceBuffer.prototype.appendWindowEnd` from MSE.
   */
  public set appendWindowEnd(appendWindowEnd: number) {
    this._checkProp("appendWindowEnd", false);
    if (isNaN(appendWindowEnd) || appendWindowEnd <= this._appendWindowStart) {
      const err = new TypeError("Invalid `appendWindowStart` set");
      throw err;
    }
    this._appendWindowEnd = appendWindowEnd;
  }

  public get appendWindowEnd(): number {
    return this._appendWindowEnd;
  }

  /**
   * Implements `SourceBuffer.prototype.buffered` from MSE.
   */
  public get buffered(): TimeRangesWithMetadata<IDummySourceBufferBufferMetadata> {
    if (this.removed) {
      const err = new Error("buffered updated on a removed DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }
    return this._buffered;
  }

  /**
   * Implements `SourceBuffer.prototype.appendBuffer` from MSE.
   * @param {BufferSource} data
   */
  public appendBuffer(data: BufferSource): void {
    if (this.removed) {
      const err = new Error("`appendBuffer` called on a removed DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this.updating) {
      const err = new Error("`appendBuffer` called on an updating DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this._callbacks.hasMediaElementErrored()) {
      const err = new Error("`appendBuffer` called on an errored HTMLMediaElement");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this._callbacks.getMediaSourceReadyState() === "ended") {
      this._callbacks.openMediaSource();
    }

    if (this.BUFFER_FULL) {
      const err = new Error("`appendBuffer` called on a full DummySourceBuffer");
      err.name = "QuotaExceededError";
      throw err;
    }

    this.updating = true;

    const canceller = new TaskCanceller();
    canceller.linkToSignal(this.canceller.signal);
    this.currentAppendCanceller = canceller;
    this.eventScheduler
      .schedule(this, "updatestart", canceller.signal)
      .then(() => {
        let u8data: Uint8Array;
        if (data instanceof Uint8Array) {
          u8data = data;
        } else if (data instanceof ArrayBuffer) {
          u8data = new Uint8Array(data);
        } else {
          u8data = new Uint8Array(data.buffer);
        }

        const segmentRanges: IRange[] = [];

        const [initSegment, otherChunks] = extractInitSegment(u8data);
        let keyId = this._lastKeyId;
        if (initSegment !== null) {
          keyId = getKeyIdFromInitSegment(initSegment);
        }
        this._lastKeyId = keyId;

        const completeChunks: Uint8Array[] =
          otherChunks === null ? [] : (extractCompleteChunks(otherChunks)[0] ?? []);
        const chunks: Uint8Array[] =
          initSegment === null ? completeChunks : [initSegment, ...completeChunks];

        for (const chunk of chunks) {
          const moovIndex = findCompleteBox(chunk, 0x6d6f6f76);
          if (moovIndex >= 0) {
            this._lastInitTimescale = getMDHDTimescale(chunk) ?? null;
          } else {
            const trackFragmentDecodeTime = getTrackFragmentDecodeTime(chunk);
            let trunDuration = getDurationFromTrun(chunk);
            if (
              trackFragmentDecodeTime !== undefined &&
              trunDuration !== undefined &&
              this._lastInitTimescale !== null
            ) {
              let startTime =
                this._timestampOffset + trackFragmentDecodeTime / this._lastInitTimescale;
              trunDuration = trunDuration / this._lastInitTimescale;
              if (startTime < 0) {
                if (trunDuration !== undefined) {
                  trunDuration += startTime; // remove from duration what comes before `0`
                }
                startTime = 0;
              }
              segmentRanges.push({ start: startTime, end: startTime + trunDuration });
            }
          }
        }

        for (const { start, end } of segmentRanges) {
          this.buffered.insert(start, end, {
            keyIds: keyId !== null ? [bytesToHex(keyId)] : null,
          });
        }
        this._callbacks.onBufferedUpdate();
        this.updating = false;
        this.hasMetadata = true;
        return this.eventScheduler.schedule(this, "update", canceller.signal);
      })
      .then(() => this.eventScheduler.schedule(this, "updateend", canceller.signal))
      .then(() => {
        this.currentAppendCanceller = null;
      })
      .catch(() => {
        this.currentAppendCanceller = null;
      });
  }

  /**
   * Implements `SourceBuffer.prototype.remove` from MSE.
   * @param {number} start
   * @param {number} end
   */
  public remove(start: number, end: number): void {
    if (this.removed) {
      const err = new Error("`remove` called on a removed DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this.updating) {
      const err = new Error("`remove` called on an updating DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    const duration = this._callbacks.getMediaSourceDuration();
    if (isNaN(duration)) {
      const err = new TypeError(
        "Cannot remove data from DummySourceBuffer: NaN duration",
      );
      throw err;
    }
    if (start < 0 || start > duration) {
      throw new TypeError("Invalid start given to `remove`");
    }

    if (isNaN(end) || start > end) {
      throw new TypeError("Invalid arguments given to `remove`");
    }

    if (this._callbacks.getMediaSourceReadyState() === "ended") {
      this._callbacks.openMediaSource();
    }

    this.updating = true;
    this._isRemoving = true;

    this.eventScheduler
      .schedule(this, "updatestart", this.canceller.signal)
      .then(() => {
        this.buffered.remove(start, end);
        this._callbacks.onBufferedUpdate();
        this.updating = false;
        this._isRemoving = false;
        return this.eventScheduler.schedule(this, "update", this.canceller.signal);
      })
      .then(() => this.eventScheduler.schedule(this, "updateend", this.canceller.signal))
      .catch(noop);
  }

  /**
   * Implements `SourceBuffer.prototype.abort` from MSE.
   */
  public abort(): void {
    if (this.removed) {
      const err = new Error("`abort` called on a removed DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this._isRemoving) {
      const err = new Error("`abort` called as a DummySourceBuffer is removing");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this.updating) {
      this.currentAppendCanceller?.cancel();
      this.currentAppendCanceller = null;
      this.updating = false;
      this.eventScheduler
        .schedule(this, "abort", this.canceller.signal)
        .then(() =>
          this.eventScheduler.schedule(this, "updateend", this.canceller.signal),
        )
        .catch(noop);
    }

    this._lastInitTimescale = null;
    this.appendWindowStart = 0;
    this.appendWindowEnd = Infinity;
  }

  /**
   * Implements `SourceBuffer.prototype.abort` from MSE.
   * @param {string} givenType
   */
  public changeType(givenType: string): void {
    const type = String(givenType);
    if (type === "") {
      throw new TypeError("`changeType` error: empty string");
    }

    if (this.removed) {
      const err = new Error("`changeType` called on a removed DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this.updating) {
      const err = new Error("`changeType` called on an updating DummySourceBuffer");
      err.name = "InvalidStateError";
      throw err;
    }

    if (this._callbacks.getMediaSourceReadyState() === "ended") {
      this._callbacks.openMediaSource();
    }
  }

  /**
   * Allows to trigger the common steps when updating a `SourceBuffer`'s
   * property.
   * @param {string} propName - The MSE name of the property you want to update.
   * @param {boolean} openMediaSource - If `true`, this property update should
   * lead to the parent `MediaSource` being open.
   */
  private _checkProp(propName: string, openMediaSource: boolean): void {
    if (this.removed) {
      const err = new Error(`${propName} updated on a removed DummySourceBuffer`);
      err.name = "InvalidStateError";
      throw err;
    }

    if (this.updating) {
      const err = new Error(`${propName} updated on an updating DummySourceBuffer`);
      err.name = "InvalidStateError";
      throw err;
    }

    if (openMediaSource) {
      if (this._callbacks.getMediaSourceReadyState() === "ended") {
        this._callbacks.openMediaSource();
      }
    }
  }
}

/**
 * Metadata linked to buffer currently present in a `DummySourceBuffer`.
 */
export interface IDummySourceBufferBufferMetadata {
  keyIds: string[] | null;
}

/**
 * Callbacks needed to instantiate a `DummyMediaSource`.
 */
interface IDummyMediaSourceCallbacks {
  /**
   * Returns `true` if the parent `HTMLMediaElement` linked to that
   * `MediaSource` has its `error` property set.
   */
  hasMediaElementErrored: () => boolean;
  /**
   * Callback called any time one of the `DummySourceBuffer` linked to this
   * `DummyMediaSource` updated its `buffered` property.
   */
  onBufferedUpdate: () => void;
  /**
   * When called, allows to update the parent `HTMLMediaElement` object's
   * `duration` property by the communicated number.
   */
  updateMediaElementDuration: (duration: number) => void;
}

/**
 * Defines the type for a `IDummySourceBufferList`, the in-JS re-implementation
 * of the `SourceBufferList` MSE Object.
 */
export type IDummySourceBufferList = ISourceBufferList &
  DummySourceBuffer[] & {
    /**
     * Callback that MUST be called any time a `DummySourceBuffer` is added to
     * this  `IDummySourceBufferList`.
     */
    _onAddSourceBuffer: () => void;
    /**
     * Callback that MUST be called any time a `DummySourceBuffer` is removed
     * from this  `IDummySourceBufferList`.
     */
    _onRemoveSourceBuffer: () => void;
  };

/**
 * Allows to create a `IDummySourceBufferList` instance, which is a
 * Re-implementation in-JS of a MSE `SourceBufferList`
 * @returns {Object}
 */
function createDummySourceBufferList(): IDummySourceBufferList {
  const list = [] as unknown as IDummySourceBufferList;
  const eventEmitter = new EventEmitter<ISourceBufferListEventMap>();

  list.onaddsourcebuffer = null;
  list.onremovesourcebuffer = null;
  list.addEventListener = eventEmitter.addEventListener.bind(eventEmitter);
  list.removeEventListener = eventEmitter.removeEventListener.bind(eventEmitter);
  list._onAddSourceBuffer = function addSourceBuffer(): void {
    const evt = new Event("addsourcebuffer");
    setTimeout(() => {
      if (typeof this.onaddsourcebuffer === "function") {
        try {
          this.onaddsourcebuffer(evt);
        } catch (_) {
          // we don't care
        }
      }
    }, 0);
    setTimeout(() => {
      /* eslint-disable-next-line */
      (eventEmitter as unknown as any).trigger("addsourcebuffer", evt);
    }, 0);
  };

  list._onRemoveSourceBuffer = function removeSourceBuffer(): void {
    const evt = new Event("removesourcebuffer");
    setTimeout(() => {
      if (typeof this.onremovesourcebuffer === "function") {
        try {
          this.onremovesourcebuffer(evt);
        } catch (_) {
          // we don't care
        }
      }
    }, 0);
    setTimeout(() => {
      /* eslint-disable-next-line */
      (eventEmitter as unknown as any).trigger("removesourcebuffer", evt);
    }, 0);
  };
  return list;
}

// NOTE: The following commented code would allow to define a format to save
// performed operations on `DummySourceBuffer` objects.
// This idea has been abandonned for now.

// export interface IStoredMediaSourceInfo {
//   sourceBuffers: IStoredSourceBufferInfo[];
// }

// export const enum SourceBufferOperation {
//   Append = 0,
//   Remove,
// }

//
// export interface ISourceBufferAppendOperation {
//   type: SourceBufferOperation.Append;
//   segment: BufferSource;
//   timestampOffset: number;
//   appendWindowStart: number;
//   appendWindowEnd: number;
//   mimeType: string;
// }

// export interface ISourceBufferRemoveOperation {
//   type: SourceBufferOperation.Remove;
//   start: number;
//   end: number;
// }

// export type ISourceBufferOperation =
//   | ISourceBufferRemoveOperation
//   | ISourceBufferAppendOperation;

// export interface IStoredSourceBufferInfo {
//   type: string;
//   operations: ISourceBufferOperation[];
// }
