diff --git a/dev/demux.html b/dev/demux.html
index 9ac4f2f..2a396f3 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -8,18 +8,48 @@
document.body.append(fileInput);
fileInput.addEventListener('change', async () => {
+ const manifest = new Mediabunny.ManifestInput({
+ entryPath: 'https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8'
+ ?? 'https://playertest.longtailvideo.com/adaptive/customIV/prog_index.m3u8'
+ ?? 'https://playertest.longtailvideo.com/adaptive/issue666/playlists/cisq0gim60007xzvi505emlxx.m3u8'
+ ?? 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8'
+ ?? 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8'
+ ?? 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8'
+ ?? 'https://cdn.jwplayer.com/manifests/pZxWPRg4.m3u8'
+ ?? 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'
+ ?? 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8',
+ getSource: path => new Mediabunny.UrlSource(path),
+ manifestFormats: Mediabunny.ALL_MANIFEST_FORMATS,
+ mediaFormats: Mediabunny.ALL_FORMATS,
+ });
+
+ const variants = await manifest.getVariants();
+ const variant = variants[2];//variants.find(x => x.path.endsWith('aac/und/stream.m3u8'));
+ const input = variant.toInput();
+ const audioTrack = await input.getPrimaryAudioTrack();
+ const sink = new Mediabunny.EncodedPacketSink(audioTrack);
+
+ for await (const packet of sink.packets()) {
+ console.log(packet.timestamp, packet.duration)
+ }
+
+ /*
const file = fileInput.files[0];
const input = new Mediabunny.Input({
formats: Mediabunny.ALL_FORMATS,
- source: new Mediabunny.BlobSource(file),
+ source: new Mediabunny.UrlSource('https://playertest.longtailvideo.com/adaptive/captions/130130211307_1.ts'),
});
- const track = await input.getPrimaryVideoTrack();
- const sink = new Mediabunny.EncodedPacketSink(track);
+ const videoTrack = await input.getPrimaryVideoTrack();
+ const sink = new Mediabunny.EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
- console.log(packet);
+ console.log("Packet", packet.timestamp)
+ for (const thing of window.iterateNalUnitsInAnnexB(packet.data)) {
+ console.log(thing, "type", window.extractNalUnitTypeForAvc(packet.data[thing.offset]))
+ }
}
+ */
/*
diff --git a/examples/media-player/media-player.ts b/examples/media-player/media-player.ts
index 6341647..b8103bc 100644
--- a/examples/media-player/media-player.ts
+++ b/examples/media-player/media-player.ts
@@ -1,9 +1,11 @@
import {
ALL_FORMATS,
+ ALL_MANIFEST_FORMATS,
AudioBufferSink,
BlobSource,
CanvasSink,
Input,
+ ManifestInput,
UrlSource,
WrappedAudioBuffer,
WrappedCanvas,
@@ -89,14 +91,44 @@ const initMediaPlayer = async (resource: File | string) => {
errorElement.textContent = '';
warningElement.textContent = '';
+ let input: Input;
+ if (typeof resource === 'string' && resource.endsWith('.m3u8')) {
+ const manifestInput = new ManifestInput({
+ entryPath: resource,
+ getSource: path => new UrlSource(path),
+ manifestFormats: ALL_MANIFEST_FORMATS,
+ mediaFormats: ALL_FORMATS,
+ });
+ const variant = (await manifestInput.getVariants())[0]!;
+
+ input = variant.toInput();// await manifestInput.toInput();
+
+ // https://test-streams.mux.dev/test_001/stream.m3u8
+ // https://test-streams.mux.dev/test_001/stream_1000k_48k_640x360_050.ts
+ } else {
+ const source = resource instanceof File
+ ? new BlobSource(resource)
+ : new UrlSource(resource);
+
+ input = new Input({
+ source,
+ formats: ALL_FORMATS,
+ });
+ }
+
+ /*
// Create an Input from the resource
const source = resource instanceof File
? new BlobSource(resource)
: new UrlSource(resource);
+ */
+
+ /*
const input = new Input({
source,
formats: ALL_FORMATS,
});
+ */
playbackTimeAtStart = 0;
totalDuration = await input.computeDuration();
diff --git a/hls-sketch.ts b/hls-sketch.ts
new file mode 100644
index 0000000..34f73c5
--- /dev/null
+++ b/hls-sketch.ts
@@ -0,0 +1,21 @@
+const hlsInput = new ManifestInput({
+ source: new UrlSource('https://example.com/playlist.m3u8'),
+ formats: [HLS],
+});
+
+const variants = await hlsInput.getVariants();
+const bestVariant = await hlsInput.getPrimaryVariant();
+const qualityVariant = await hlsInput.getVariantFor('1080p'); // In spirit
+
+bestVariant.tracks; // ?
+bestVariant.bitrate; // ?
+bestVariant.isLive(); // ?
+
+const input = variant.toInput(); // => Input
+const segments = variant.getSegments(); // => Segments[]?
+
+const segment = variant.getFirstSegment();
+segment.path; // => string (for example 'segment1.ts')
+segment.timestamp; // => 10
+segment.duration; // 5
+segment.toInput(); // => Input
diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts
index 18af576..cfcd077 100644
--- a/src/adts/adts-demuxer.ts
+++ b/src/adts/adts-demuxer.ts
@@ -251,6 +251,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
return numberOfChannels;
}
+ getVariant() {
+ return null;
+ }
+
getSampleRate() {
assert(this.demuxer.firstFrameHeader);
diff --git a/src/aes.ts b/src/aes.ts
new file mode 100644
index 0000000..7af4370
--- /dev/null
+++ b/src/aes.ts
@@ -0,0 +1,294 @@
+/*!
+ * Copyright (c) 2026-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import { assert, MaybePromise } from './misc';
+import { readBytes, Reader } from './reader';
+
+// Inspired in part by https://github.com/halloweeks/AES-128-CBC/blob/main/AES_128_CBC.h
+
+export const AES_128_BLOCK_SIZE = 16;
+
+const Te4 = new Uint32Array(256);
+const Td0 = new Uint32Array(256);
+const Td1 = new Uint32Array(256);
+const Td2 = new Uint32Array(256);
+const Td3 = new Uint32Array(256);
+const Td4 = new Uint32Array(256);
+const rcon = new Uint32Array(10);
+
+let tablesGenerated = false;
+
+// Generating the tables once is much more bundle size-efficient than shipping them in the bundle (entropy ftw)
+const generateAesTables = () => {
+ const sbox = new Uint8Array(256);
+ const log = new Uint8Array(256);
+ const pow = new Uint8Array(256);
+
+ // 1. Generate GF(2^8) log/exp tables
+ // Primitive polynomial: x^8 + x^4 + x^3 + x + 1 (0x11B)
+ for (let i = 0, p = 1; i < 256; i++) {
+ pow[i] = p;
+ log[p] = i;
+ p = p ^ (p << 1) ^ (p & 0x80 ? 0x11B : 0);
+ }
+
+ // Helper: GF(2^8) multiplication
+ const mul = (a: number, b: number) =>
+ (a && b) ? pow[(log[a]! + log[b]!) % 255]! : 0;
+
+ // 2. Generate S-Box and Inverse S-Box
+ sbox[0] = 0x63; // Special case for 0
+ // Loop for inverse (using log/exp) and Affine Transform
+ for (let i = 1; i < 256; i++) {
+ const x = pow[255 - log[i]!]!; // Multiplicative inverse
+ let s = x ^ (x << 1) ^ (x << 2) ^ (x << 3) ^ (x << 4);
+ s = (s >>> 8) ^ (s & 0xFF) ^ 0x63; // Affine transform
+ sbox[i] = s;
+ }
+
+ // 3. Fill Tables
+ for (let i = 0; i < 256; i++) {
+ const s = sbox[i]!; // Forward S-Box value
+ const is = sbox.indexOf(i); // Inverse S-Box value
+
+ // Te4: Forward S-Box packed
+ Te4[i] = (s << 24) | (s << 16) | (s << 8) | s;
+
+ // Td4: Inverse S-Box packed
+ Td4[i] = (is << 24) | (is << 16) | (is << 8) | is;
+
+ // Td0-Td3: Inverse MixColumns applied to Inverse S-Box
+ // Coefficients: 0x0E, 0x09, 0x0D, 0x0B (Order specific to Td0 structure)
+ const b0 = mul(is, 0x0E);
+ const b1 = mul(is, 0x09);
+ const b2 = mul(is, 0x0D);
+ const b3 = mul(is, 0x0B);
+
+ const w = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
+ Td0[i] = w;
+ Td1[i] = (w >>> 8) | (w << 24); // Rotate right 8
+ Td2[i] = (w >>> 16) | (w << 16); // Rotate right 16
+ Td3[i] = (w >>> 24) | (w << 8); // Rotate right 24
+ }
+
+ // 4. Generate Rcon
+ let r = 1;
+ for (let i = 0; i < 10; i++) {
+ rcon[i] = r << 24;
+ r = (r << 1) ^ (r & 0x80 ? 0x11B : 0);
+ }
+
+ tablesGenerated = true;
+};
+
+export type Aes128CbcContextInit = {
+ key: Uint8Array;
+ iv: Uint8Array;
+};
+
+/** A context for doing AES-128-CBC operations. Better than the Web Crypto API since we can stream it. */
+export class Aes128CbcContext {
+ roundkey = new Uint32Array(44);
+ iv = new Uint32Array(AES_128_BLOCK_SIZE / Uint32Array.BYTES_PER_ELEMENT);
+ in = new Uint8Array(AES_128_BLOCK_SIZE);
+ out = new Uint8Array(AES_128_BLOCK_SIZE);
+ inView = new DataView(this.in.buffer);
+ outView = new DataView(this.out.buffer);
+
+ init({ key, iv }: Aes128CbcContextInit) {
+ assert(key.byteLength === 16);
+ assert(iv.byteLength === 16);
+
+ if (!tablesGenerated) {
+ generateAesTables();
+ }
+
+ const keyView = new DataView(key.buffer, key.byteOffset, key.byteLength);
+ const ivView = new DataView(iv.buffer, iv.byteOffset, iv.byteLength);
+
+ this.roundkey[0] = keyView.getUint32(0, false);
+ this.roundkey[1] = keyView.getUint32(4, false);
+ this.roundkey[2] = keyView.getUint32(8, false);
+ this.roundkey[3] = keyView.getUint32(12, false);
+
+ this.iv[0] = ivView.getUint32(0, false);
+ this.iv[1] = ivView.getUint32(4, false);
+ this.iv[2] = ivView.getUint32(8, false);
+ this.iv[3] = ivView.getUint32(12, false);
+
+ for (let index = 4; index < 44; index += 4) {
+ const temp = this.roundkey[index - 1]!;
+ this.roundkey[index] = this.roundkey[index - 4]!
+ ^ (Te4[(temp >>> 16) & 0xff]! & 0xff000000)
+ ^ (Te4[(temp >>> 8) & 0xff]! & 0x00ff0000)
+ ^ (Te4[(temp >>> 0) & 0xff]! & 0x0000ff00)
+ ^ (Te4[(temp >>> 24) & 0xff]! & 0x000000ff)
+ ^ rcon[(index / 4) - 1]!;
+ this.roundkey[index + 1] = this.roundkey[index - 3]! ^ this.roundkey[index]!;
+ this.roundkey[index + 2] = this.roundkey[index - 2]! ^ this.roundkey[index + 1]!;
+ this.roundkey[index + 3] = this.roundkey[index - 1]! ^ this.roundkey[index + 2]!;
+ }
+
+ // Invert the order of the round keys
+ for (let i = 0, j = 40; i < j; i += 4, j -= 4) {
+ for (let k = 0; k < 4; k++) {
+ const temp = this.roundkey[i + k]!;
+ this.roundkey[i + k] = this.roundkey[j + k]!;
+ this.roundkey[j + k] = temp;
+ }
+ }
+
+ // Apply Inverse MixColumn transform to all round keys except first and last
+ for (let index = 4; index < 40; index += 4) {
+ for (let k = 0; k < 4; k++) {
+ const rk = this.roundkey[index + k]!;
+ this.roundkey[index + k]
+ = Td0[Te4[(rk >>> 24) & 0xff]! & 0xff]!
+ ^ Td1[Te4[(rk >>> 16) & 0xff]! & 0xff]!
+ ^ Td2[Te4[(rk >>> 8) & 0xff]! & 0xff]!
+ ^ Td3[Te4[(rk >>> 0) & 0xff]! & 0xff]!;
+ }
+ }
+ }
+
+ decrypt() {
+ let s0 = this.inView.getUint32(0, false) ^ this.roundkey[0]!;
+ let s1 = this.inView.getUint32(4, false) ^ this.roundkey[1]!;
+ let s2 = this.inView.getUint32(8, false) ^ this.roundkey[2]!;
+ let s3 = this.inView.getUint32(12, false) ^ this.roundkey[3]!;
+
+ // Store input for CBC XOR later
+ const temp0 = this.inView.getUint32(0, false);
+ const temp1 = this.inView.getUint32(4, false);
+ const temp2 = this.inView.getUint32(8, false);
+ const temp3 = this.inView.getUint32(12, false);
+
+ let t0, t1, t2, t3;
+
+ // Rounds 1-9
+ for (let round = 1; round < 10; round++) {
+ const offset = round * 4;
+ t0 = Td0[s0 >>> 24]!
+ ^ Td1[(s3 >>> 16) & 0xff]!
+ ^ Td2[(s2 >>> 8) & 0xff]!
+ ^ Td3[s1 & 0xff]!
+ ^ this.roundkey[offset]!;
+ t1 = Td0[s1 >>> 24]!
+ ^ Td1[(s0 >>> 16) & 0xff]!
+ ^ Td2[(s3 >>> 8) & 0xff]!
+ ^ Td3[s2 & 0xff]!
+ ^ this.roundkey[offset + 1]!;
+ t2 = Td0[s2 >>> 24]!
+ ^ Td1[(s1 >>> 16) & 0xff]!
+ ^ Td2[(s0 >>> 8) & 0xff]!
+ ^ Td3[s3 & 0xff]!
+ ^ this.roundkey[offset + 2]!;
+ t3 = Td0[s3 >>> 24]!
+ ^ Td1[(s2 >>> 16) & 0xff]!
+ ^ Td2[(s1 >>> 8) & 0xff]!
+ ^ Td3[s0 & 0xff]!
+ ^ this.roundkey[offset + 3]!;
+
+ s0 = t0;
+ s1 = t1;
+ s2 = t2;
+ s3 = t3;
+ }
+
+ // Final Round (10)
+ const f0 = (Td4[(s0 >>> 24) & 0xff]! & 0xff000000)
+ ^ (Td4[(s3 >>> 16) & 0xff]! & 0x00ff0000)
+ ^ (Td4[(s2 >>> 8) & 0xff]! & 0x0000ff00)
+ ^ (Td4[(s1 >>> 0) & 0xff]! & 0x000000ff)
+ ^ this.roundkey[40]!;
+ const f1 = (Td4[(s1 >>> 24) & 0xff]! & 0xff000000)
+ ^ (Td4[(s0 >>> 16) & 0xff]! & 0x00ff0000)
+ ^ (Td4[(s3 >>> 8) & 0xff]! & 0x0000ff00)
+ ^ (Td4[(s2 >>> 0) & 0xff]! & 0x000000ff)
+ ^ this.roundkey[41]!;
+ const f2 = (Td4[(s2 >>> 24) & 0xff]! & 0xff000000)
+ ^ (Td4[(s1 >>> 16) & 0xff]! & 0x00ff0000)
+ ^ (Td4[(s0 >>> 8) & 0xff]! & 0x0000ff00)
+ ^ (Td4[(s3 >>> 0) & 0xff]! & 0x000000ff)
+ ^ this.roundkey[42]!;
+ const f3 = (Td4[(s3 >>> 24) & 0xff]! & 0xff000000)
+ ^ (Td4[(s2 >>> 16) & 0xff]! & 0x00ff0000)
+ ^ (Td4[(s1 >>> 8) & 0xff]! & 0x0000ff00)
+ ^ (Td4[(s0 >>> 0) & 0xff]! & 0x000000ff)
+ ^ this.roundkey[43]!;
+
+ // CBC XOR and output
+ this.outView.setUint32(0, f0 ^ this.iv[0]!, false);
+ this.outView.setUint32(4, f1 ^ this.iv[1]!, false);
+ this.outView.setUint32(8, f2 ^ this.iv[2]!, false);
+ this.outView.setUint32(12, f3 ^ this.iv[3]!, false);
+
+ // Update IV for next block
+ this.iv[0] = temp0;
+ this.iv[1] = temp1;
+ this.iv[2] = temp2;
+ this.iv[3] = temp3;
+ }
+}
+
+export const createAesDecryptStream = (reader: Reader, getInit: () => MaybePromise) => {
+ let initted = false;
+ let pos = 0;
+ const CHUNK_SIZE = 2 ** 16;
+ const BLOCK_SIZE = 16;
+
+ const aesContext = new Aes128CbcContext();
+
+ return new ReadableStream({
+ pull: async (controller) => {
+ if (!initted) {
+ aesContext.init(await getInit());
+ initted = true;
+ }
+
+ const requestedLength = CHUNK_SIZE + BLOCK_SIZE;
+
+ let nextSlice = reader.requestSliceRange(pos, 0, requestedLength);
+ if (nextSlice instanceof Promise) nextSlice = await nextSlice;
+ if (!nextSlice || nextSlice.length === 0) {
+ // Due to padding, this should never happen
+ throw new Error('Invalid ciphertext.');
+ }
+
+ const sliceLength = nextSlice.length;
+ if (sliceLength % 16 !== 0) {
+ throw new Error('Invalid ciphertext.');
+ }
+
+ const bytesToRead = sliceLength === requestedLength
+ ? sliceLength - BLOCK_SIZE // Don't read the last block
+ : sliceLength;
+
+ const input = readBytes(nextSlice, bytesToRead);
+ const output = new Uint8Array(bytesToRead);
+
+ for (let i = 0; i < bytesToRead; i += 16) {
+ aesContext.in.set(input.subarray(i, i + 16));
+ aesContext.decrypt();
+ output.set(aesContext.out, i);
+ }
+
+ if (bytesToRead < sliceLength) {
+ controller.enqueue(output);
+ pos += bytesToRead;
+ } else {
+ // This is the last chunk
+ const paddingLength = output[bytesToRead - 1]!;
+ const trimmedOutput = output.subarray(0, bytesToRead - paddingLength); // PKCS#7 padding
+
+ controller.enqueue(trimmedOutput);
+ controller.close();
+ }
+ },
+ });
+};
diff --git a/src/aggregate-demuxer.ts b/src/aggregate-demuxer.ts
new file mode 100644
index 0000000..b46c71d
--- /dev/null
+++ b/src/aggregate-demuxer.ts
@@ -0,0 +1,202 @@
+import { VideoCodec, AudioCodec } from './codec';
+import { Demuxer } from './demuxer';
+import { Input } from './input';
+import {
+ InputTrack,
+ InputVideoTrack,
+ InputAudioTrack,
+ InputTrackBacking,
+ InputVideoTrackBacking,
+ InputAudioTrackBacking,
+} from './input-track';
+import { ManifestInputVariant } from './manifest-input-variant';
+import { PacketRetrievalOptions } from './media-sink';
+import { MetadataTags, TrackDisposition } from './metadata';
+import { arrayCount, Rotation } from './misc';
+import { EncodedPacket } from './packet';
+
+/** A utility demuxer that acts as the union of multiple Inputs. */
+export class InputAggregateDemuxer extends Demuxer {
+ subInputs: Input[];
+ tracksPromise: Promise | null = null;
+
+ constructor(input: Input, subInputs: Input[]) {
+ super(input);
+
+ this.subInputs = subInputs;
+ }
+
+ async computeDuration() {
+ const durations = await Promise.all(this.subInputs.map(x => x.computeDuration()));
+ return Math.max(0, ...durations);
+ }
+
+ async getMetadataTags(): Promise {
+ return {}; // todo?
+ }
+
+ async getMimeType() {
+ return ''; // todo?
+ }
+
+ getTracks() {
+ return this.tracksPromise ??= (async () => {
+ const subInputTracks = await Promise.all(this.subInputs.map(x => x.getTracks()));
+
+ const tracks: InputTrack[] = [];
+ for (const inputTracks of subInputTracks) {
+ for (const track of inputTracks) {
+ if (track.isVideoTrack()) {
+ const number = arrayCount(tracks, x => x.type === 'video') + 1;
+ tracks.push(new InputVideoTrack(
+ this.input,
+ new InputAggregateVideoTrackBacking(track._backing, number),
+ ));
+ } else if (track.isAudioTrack()) {
+ const number = arrayCount(tracks, x => x.type === 'audio') + 1;
+ tracks.push(new InputAudioTrack(
+ this.input,
+ new InputAggregateAudioTrackBacking(track._backing, number),
+ ));
+ }
+ }
+ }
+
+ return tracks;
+ })();
+ }
+}
+
+class InputAggregateTrackBacking implements InputTrackBacking {
+ source: InputTrackBacking;
+ number: number;
+
+ constructor(source: InputTrackBacking, number: number) {
+ this.source = source;
+ this.number = number;
+ }
+
+ getId() {
+ return this.source.getId();
+ }
+
+ getNumber() {
+ return this.number;
+ }
+
+ getCodec() {
+ return this.source.getCodec();
+ }
+
+ getInternalCodecId() {
+ return this.source.getInternalCodecId();
+ }
+
+ getName() {
+ return this.source.getName();
+ }
+
+ getLanguageCode() {
+ return this.source.getLanguageCode();
+ }
+
+ getTimeResolution() {
+ return this.source.getTimeResolution();
+ }
+
+ getDisposition(): TrackDisposition {
+ return this.source.getDisposition();
+ }
+
+ getVariant(): ManifestInputVariant | null {
+ return this.source.getVariant();
+ }
+
+ getFirstTimestamp() {
+ return this.source.getFirstTimestamp();
+ }
+
+ computeDuration() {
+ return this.source.computeDuration();
+ }
+
+ getFirstPacket(options: PacketRetrievalOptions): Promise {
+ return this.source.getFirstPacket(options);
+ }
+
+ getPacket(timestamp: number, options: PacketRetrievalOptions): Promise {
+ return this.source.getPacket(timestamp, options);
+ }
+
+ getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
+ return this.source.getNextPacket(packet, options);
+ }
+
+ getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise {
+ return this.source.getKeyPacket(timestamp, options);
+ }
+
+ getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
+ return this.source.getNextKeyPacket(packet, options);
+ }
+}
+
+class InputAggregateVideoTrackBacking extends InputAggregateTrackBacking implements InputVideoTrackBacking {
+ override source!: InputVideoTrackBacking;
+
+ constructor(source: InputVideoTrackBacking, number: number) {
+ super(source, number);
+ }
+
+ override getCodec(): VideoCodec | null {
+ return this.source.getCodec();
+ }
+
+ getCodedWidth() {
+ return this.source.getCodedWidth();
+ }
+
+ getCodedHeight() {
+ return this.source.getCodedHeight();
+ }
+
+ getRotation(): Rotation {
+ return this.source.getRotation();
+ }
+
+ getColorSpace() {
+ return this.source.getColorSpace();
+ }
+
+ canBeTransparent() {
+ return this.source.canBeTransparent();
+ }
+
+ getDecoderConfig() {
+ return this.source.getDecoderConfig();
+ }
+}
+
+class InputAggregateAudioTrackBacking extends InputAggregateTrackBacking implements InputAudioTrackBacking {
+ override source!: InputAudioTrackBacking;
+
+ constructor(source: InputAudioTrackBacking, number: number) {
+ super(source, number);
+ }
+
+ override getCodec(): AudioCodec | null {
+ return this.source.getCodec();
+ }
+
+ getNumberOfChannels() {
+ return this.source.getNumberOfChannels();
+ }
+
+ getSampleRate() {
+ return this.source.getSampleRate();
+ }
+
+ getDecoderConfig() {
+ return this.source.getDecoderConfig();
+ }
+}
diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts
index 4547c4b..40a650a 100644
--- a/src/flac/flac-demuxer.ts
+++ b/src/flac/flac-demuxer.ts
@@ -575,6 +575,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
};
}
+ getVariant() {
+ return null;
+ }
+
async getFirstTimestamp() {
return 0;
}
diff --git a/src/index.ts b/src/index.ts
index 1ee6ded..d353898 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -223,5 +223,13 @@ export {
AttachedFile,
TrackDisposition,
} from './metadata';
+export {
+ ManifestInput,
+ ManifestInputOptions,
+} from './manifest-input';
+export {
+ M3U8,
+ ALL_MANIFEST_FORMATS,
+} from './manifest-input-format';
// 🐡🦔
diff --git a/src/input-format.ts b/src/input-format.ts
index f5c52e9..fa437d2 100644
--- a/src/input-format.ts
+++ b/src/input-format.ts
@@ -542,6 +542,37 @@ export class MpegTsInputFormat extends InputFormat {
}
}
+export class VirtualInputFormat extends InputFormat {
+ /** @internal */
+ _createDemuxerFn: (input: Input) => Demuxer;
+
+ /** @internal */
+ constructor(createDemuxer: (input: Input) => Demuxer) {
+ super();
+ this._createDemuxerFn = createDemuxer;
+ }
+
+ /** @internal */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async _canReadInput(input: Input) {
+ return true;
+ }
+
+ /** @internal */
+
+ _createDemuxer(input: Input): Demuxer {
+ return this._createDemuxerFn(input);
+ }
+
+ get name() {
+ return 'Virtual input format';
+ }
+
+ get mimeType() {
+ return 'application/magic';
+ }
+}
+
/**
* MP4 input format singleton.
* @group Input formats
diff --git a/src/input-track.ts b/src/input-track.ts
index 607e56a..239a471 100644
--- a/src/input-track.ts
+++ b/src/input-track.ts
@@ -15,6 +15,7 @@ import { assert, Rotation } from './misc';
import { TrackType } from './output';
import { EncodedPacket, PacketType } from './packet';
import { TrackDisposition } from './metadata';
+import { ManifestInputVariant } from './manifest-input-variant';
/**
* Contains aggregate statistics about the encoded packets of a track.
@@ -39,6 +40,8 @@ export interface InputTrackBacking {
getLanguageCode(): string;
getTimeResolution(): number;
getDisposition(): TrackDisposition;
+ getVariant(): ManifestInputVariant | null;
+
getFirstTimestamp(): Promise;
computeDuration(): Promise;
@@ -146,6 +149,10 @@ export abstract class InputTrack {
return this._backing.getDisposition();
}
+ get variant() {
+ return this._backing.getVariant();
+ }
+
/**
* Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value
* may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples
diff --git a/src/input.ts b/src/input.ts
index b1ce3ca..b6ba44b 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -41,7 +41,7 @@ export class Input implements Disposable {
/** @internal */
_format: InputFormat | null = null;
/** @internal */
- _reader: Reader;
+ _reader!: Reader;
/** @internal */
_disposed = false;
@@ -65,18 +65,17 @@ export class Input implements Disposable {
throw new TypeError('options.source must be a Source.');
}
if (options.source._disposed) {
- throw new Error('options.source must not be disposed.');
+ throw new TypeError('options.source must not be disposed.');
}
this._formats = options.formats;
this._source = options.source;
- this._reader = new Reader(options.source);
}
/** @internal */
_getDemuxer() {
return this._demuxerPromise ??= (async () => {
- this._reader.fileSize = await this._source.getSizeOrNull();
+ this._reader = await Reader.fromSource(this._source);
for (const format of this._formats) {
const canRead = await format._canReadInput(this);
@@ -86,6 +85,7 @@ export class Input implements Disposable {
}
}
+ console.log(this._source);
throw new Error('Input has an unsupported or unrecognizable format.');
})();
}
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index 951b728..910446c 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -2378,6 +2378,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
return this.internalTrack.disposition;
}
+ getVariant() {
+ return null;
+ }
+
async computeDuration() {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
diff --git a/src/m3u8/m3u8-parser.ts b/src/m3u8/m3u8-parser.ts
new file mode 100644
index 0000000..68b6d95
--- /dev/null
+++ b/src/m3u8/m3u8-parser.ts
@@ -0,0 +1,515 @@
+/*!
+ * Copyright (c) 2026-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import { AES_128_BLOCK_SIZE } from '../aes';
+import { ManifestInput } from '../manifest-input';
+import { ManifestParser } from '../manifest-parser';
+import { ManifestInputVariant } from '../manifest-input-variant';
+import { AsyncMutex, binarySearchLessOrEqual, joinPaths, last, toDataView } from '../misc';
+import { LineReader, Reader } from '../reader';
+import { ManifestInputSegment, ManifestInputSegmentLocation, SegmentEncryptionInfo } from '../manifest-input-segment';
+import { inferCodecFromCodecString, MediaCodec } from '../codec';
+
+const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
+
+export class M3u8Parser extends ManifestParser {
+ _metadataPromise: Promise | null = null;
+ _variants: M3u8ManifestVariant[] = [];
+ _lineReader: LineReader;
+
+ constructor(input: ManifestInput) {
+ super(input);
+ this._lineReader = new LineReader(() => input._entryReader, isM3u8Comment);
+ }
+
+ _readMetadata() {
+ return this._metadataPromise ??= (async () => {
+ let line = this._lineReader.readNextLine();
+ if (line instanceof Promise) line = await line;
+
+ if (line !== '#EXTM3U') {
+ throw new Error('Invalid M3U8 file; expected first line to be #EXTM3U.');
+ }
+
+ let iFramesOnlyTagFound = false;
+
+ while (true) {
+ let line = this._lineReader.readNextLine();
+ if (line instanceof Promise) line = await line;
+
+ if (line === null) {
+ break;
+ }
+
+ if (line.startsWith('#EXT-X-STREAM-INF:')) {
+ let playlistPath = this._lineReader.readNextLine();
+ if (playlistPath instanceof Promise) playlistPath = await playlistPath;
+
+ if (playlistPath === null) {
+ throw new Error('Incorrect M3U8 file; a line must follow the #EXT-X-STREAM-INF tag.');
+ }
+
+ const fullPath = joinPaths(this._input._entryPath, playlistPath);
+ const attributes = new AttributeList(line.slice(18));
+
+ this._variants.push(new M3u8ManifestVariant(
+ this,
+ fullPath,
+ null,
+ attributes,
+ false,
+ ));
+ } else if (line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
+ const attributes = new AttributeList(line.slice(18));
+ const playlistPath = attributes.get('uri');
+
+ if (playlistPath === null) {
+ throw new Error(
+ 'Invalid M3U8 file; #EXT-X-I-FRAME-STREAM-INF tag requires a URI attribute.',
+ );
+ }
+
+ const fullPath = joinPaths(this._input._entryPath, playlistPath);
+
+ this._variants.push(new M3u8ManifestVariant(
+ this,
+ fullPath,
+ null,
+ attributes,
+ true,
+ ));
+ } else if (line.startsWith('#EXT-X-MEDIA:')) {
+ const attributes = new AttributeList(line.slice(13));
+
+ const groupId = attributes.get('group-id');
+ if (groupId === null) {
+ throw new Error(
+ 'Invalid M3U8 file; #EXT-X-MEDIA tag requires a GROUP-ID attribute.',
+ );
+ }
+
+ const uri = attributes.get('uri');
+ if (uri === null) {
+ continue;
+ }
+
+ const fullPath = joinPaths(this._input._entryPath, uri);
+
+ this._variants.push(new M3u8ManifestVariant(
+ this,
+ fullPath,
+ null,
+ attributes,
+ false,
+ ));
+ } else if (line === '#EXT-X-I-FRAMES-ONLY') {
+ iFramesOnlyTagFound = true;
+ } else if (line.startsWith('#EXTINF:')) {
+ // This is a media playlist, not a master playlist
+
+ this._variants = [
+ new M3u8ManifestVariant(
+ this,
+ this._input._entryPath,
+ this._lineReader.reader,
+ new AttributeList(''),
+ iFramesOnlyTagFound,
+ ),
+ ];
+
+ break;
+ }
+ }
+ })();
+ }
+
+ override async getVariants() {
+ await this._readMetadata();
+ return this._variants;
+ }
+}
+
+export class M3u8ManifestVariant extends ManifestInputVariant {
+ _attributes: AttributeList;
+ _isKeyFrameOnly: boolean;
+ _parser: M3u8Parser;
+ _lineReader: LineReader;
+ _segments: ManifestInputSegment[] = [];
+ _nextSegmentDuration: number | null = null;
+ _nextSegmentTitle: string | null = null;
+ _accumulatedTime = 0;
+ _headerRead = false;
+ _mutex = new AsyncMutex();
+ _currentKey: SegmentEncryptionInfo | null = null;
+ _nextSequenceNumber = 0;
+ _currentInitSegment: ManifestInputSegment | null = null;
+ _lastByteRangeEnd: number | null = null;
+ _nextByteRange: { offset: number; length: number } | null = null;
+
+ /** @internal */
+ constructor(
+ parser: M3u8Parser,
+ path: string,
+ reader: Reader | null,
+ attributes: AttributeList,
+ isKeyFrameOnly: boolean,
+ ) {
+ super(parser._input, path);
+
+ this._attributes = attributes;
+ this._isKeyFrameOnly = isKeyFrameOnly;
+ this._parser = parser;
+
+ if (reader) {
+ this._lineReader = new LineReader(() => reader, isM3u8Comment);
+ } else {
+ this._lineReader = new LineReader(async () => {
+ const source = await this.input._getSourceUncached(this.path);
+ return Reader.fromSource(source);
+ }, isM3u8Comment);
+ }
+ }
+
+ get metadata() {
+ const codecStrings = this._getCodecStrings();
+ const codecs = codecStrings.map(x => inferCodecFromCodecString(x)).filter(Boolean) as MediaCodec[];
+
+ return {
+ name: this._attributes.get('name'),
+ bitrate: this._attributes.getAsNumber('bandwidth'),
+ averageBitrate: this._attributes.getAsNumber('average-bandwidth'),
+ codecs,
+ codecStrings,
+ resolution: this._getResolution(),
+ frameRate: this._attributes.getAsNumber('frame-rate'),
+ isKeyFrameOnly: this._isKeyFrameOnly,
+ };
+ }
+
+ get groupId() {
+ return this._attributes.get('group-id');
+ }
+
+ get associatedGroupId() {
+ return this._attributes.get('video')
+ ?? this._attributes.get('audio')
+ ?? this._attributes.get('subtitles')
+ ?? this._attributes.get('closed-captions');
+ }
+
+ _getCodecStrings() {
+ const value = this._attributes.get('codecs');
+ if (!value) {
+ return [];
+ }
+
+ return value.split(',').map(x => x.trim()).filter(x => x);
+ }
+
+ _getResolution() {
+ const value = this._attributes.get('resolution');
+ if (!value) {
+ return null;
+ }
+
+ const match = value.match(/^(\d+)x(\d+)$/);
+ if (!match) {
+ return null;
+ }
+
+ return {
+ width: Number(match[1]),
+ height: Number(match[2]),
+ };
+ }
+
+ async getFirstSegment() {
+ if (this._segments.length === 0) {
+ await this._readNextSegment();
+ }
+
+ return this._segments[0] ?? null;
+ }
+
+ async getSegmentAt(relativeTimestamp: number) {
+ await this._readUntilSegmentAt(relativeTimestamp);
+
+ const index = binarySearchLessOrEqual(this._segments, relativeTimestamp, x => x.relativeTimestamp);
+ if (index === -1) {
+ return null;
+ }
+
+ return this._segments[index]!;
+ }
+
+ async getNextSegment(segment: ManifestInputSegment) {
+ const index = this._segments.indexOf(segment);
+ if (index === -1) {
+ throw new Error('Segment was not created by this variant.');
+ }
+
+ if (index + 1 < this._segments.length) {
+ return this._segments[index + 1]!;
+ }
+
+ if (this._lineReader.reachedEnd) {
+ return null;
+ }
+
+ await this._readNextSegment();
+ return this._segments[index + 1] ?? null;
+ }
+
+ async getPreviousSegment(segment: ManifestInputSegment): Promise {
+ const index = this._segments.indexOf(segment);
+ if (index === -1) {
+ throw new Error('Segment was not created by this variant.');
+ }
+
+ if (index - 1 >= 0) {
+ return this._segments[index - 1]!;
+ }
+
+ return null;
+ }
+
+ async _readNextSegment() {
+ const segmentCount = this._segments.length;
+ const release = await this._mutex.acquire();
+
+ try {
+ if (segmentCount < this._segments.length) {
+ // The next segment has already been read by someone else, great
+ return;
+ }
+
+ while (true) {
+ let line = this._lineReader.readNextLine();
+ if (line instanceof Promise) line = await line;
+
+ if (line === null) {
+ return;
+ }
+
+ if (!this._headerRead) {
+ if (line !== '#EXTM3U') {
+ throw new Error('Invalid M3U8 file; expected first line to be #EXTM3U.');
+ }
+
+ this._headerRead = true;
+ continue;
+ }
+
+ if (!line.startsWith('#')) {
+ if (this._nextSegmentDuration === null) {
+ throw new Error('Invalid M3U8 file; a segment must be preceeded by a #EXTINF tag.');
+ }
+
+ let key = this._currentKey;
+ if (key && !key.iv) {
+ // "the Media Sequence Number is to be used as the IV when decrypting a Media Segment, by
+ // putting its big-endian binary representation into a 16-octet (128-bit) buffer and padding
+ // (on the left) with zeros"
+
+ const iv = new Uint8Array(AES_128_BLOCK_SIZE);
+ const view = toDataView(iv);
+ view.setUint32(8, Math.floor(this._nextSequenceNumber / (2 ** 32)));
+ view.setUint32(12, this._nextSequenceNumber);
+
+ key = { ...key, iv };
+ }
+
+ const fullPath = joinPaths(this.path, line);
+ const location: ManifestInputSegmentLocation = {
+ path: fullPath,
+ offset: this._nextByteRange?.offset ?? 0,
+ length: this._nextByteRange?.length ?? null,
+ };
+
+ const segment = new ManifestInputSegment(
+ this,
+ location,
+ this._accumulatedTime,
+ this._nextSegmentDuration,
+ this._nextSegmentTitle,
+ key,
+ this._currentInitSegment,
+ );
+ this._segments.push(segment);
+ this._accumulatedTime += this._nextSegmentDuration;
+ this._nextSequenceNumber++;
+ this._currentInitSegment ??= segment;
+
+ this._nextSegmentDuration = null;
+ this._nextSegmentTitle = null;
+
+ this._lastByteRangeEnd = this._nextByteRange
+ ? this._nextByteRange.offset + this._nextByteRange.length
+ : null;
+ this._nextByteRange = null;
+
+ return;
+ }
+
+ if (line.startsWith('#EXTINF:')) {
+ const extinfContent = line.slice(8);
+ const commaIndex = extinfContent.indexOf(',');
+ const durationStr = commaIndex === -1 ? extinfContent : extinfContent.slice(0, commaIndex);
+ const duration = Number(durationStr);
+ if (!Number.isFinite(duration) || duration < 0) {
+ throw new Error(`Invalid #EXTINF tag duration '${durationStr}'.`);
+ }
+ const title = commaIndex === -1 ? null : extinfContent.slice(commaIndex + 1).trim() || null;
+
+ this._nextSegmentDuration = duration;
+ this._nextSegmentTitle = title;
+ } else if (line.startsWith('#EXT-X-KEY:')) {
+ const attributes = new AttributeList(line.slice(11));
+ const method = attributes.get('method');
+
+ if (method === 'NONE') {
+ this._currentKey = null;
+ } else if (method === 'AES-128') {
+ const uri = attributes.get('uri');
+ if (!uri) {
+ throw new Error('Invalid #EXT-X-KEY: AES-128 requires a URI attribute.');
+ }
+
+ let iv: Uint8Array | null = null;
+ const ivString = attributes.get('iv');
+ if (ivString) {
+ if (!IV_STRING_REGEX.test(ivString)) {
+ throw new Error(`Unsupported IV format '${ivString}'.`);
+ }
+
+ let hex = ivString.slice(2);
+ hex = hex.padStart(AES_128_BLOCK_SIZE * 2, '0');
+
+ iv = new Uint8Array(AES_128_BLOCK_SIZE);
+ for (let i = 0; i < AES_128_BLOCK_SIZE; i++) {
+ const startIndex = -AES_128_BLOCK_SIZE * 2 + i;
+ iv[i] = parseInt(hex.slice(startIndex, startIndex + 2), 16);
+ }
+ }
+
+ this._currentKey = {
+ method: 'AES-128',
+ keyUri: joinPaths(this.path, uri),
+ iv,
+ keyFormat: attributes.get('keyformat') ?? 'identity',
+ };
+ } else {
+ throw new Error(`Unsupported encryption method '${method}'.`);
+ }
+ } else if (line.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
+ const value = line.slice(22);
+ const number = Number(value);
+
+ if (!Number.isInteger(number) || number < 0) {
+ throw new Error(`Invalid EXT-X-MEDIA-SEQUENCE value '${value}'.`);
+ }
+
+ this._nextSequenceNumber = number;
+ } else if (line.startsWith('#EXT-X-BYTERANGE:')) {
+ const content = line.slice(17);
+ const atIndex = content.indexOf('@');
+
+ const length = Number(atIndex === -1 ? content : content.slice(0, atIndex));
+ if (!Number.isInteger(length) || length < 0) {
+ throw new Error(`Invalid #EXT-X-BYTERANGE length '${content}'.`);
+ }
+
+ let offset: number;
+ if (atIndex !== -1) {
+ offset = Number(content.slice(atIndex + 1));
+ if (!Number.isInteger(offset) || offset < 0) {
+ throw new Error(`Invalid #EXT-X-BYTERANGE offset '${content}'.`);
+ }
+ } else {
+ if (this._lastByteRangeEnd === null) {
+ throw new Error(
+ 'Invalid M3U8 file; #EXT-X-BYTERANGE without offset requires a previous byte range.',
+ );
+ }
+ offset = this._lastByteRangeEnd;
+ }
+
+ this._nextByteRange = { offset, length };
+ } else if (line.startsWith('#EXT-X-DISCONTINUITY')) {
+ this._currentInitSegment = null;
+ }
+ }
+ } finally {
+ release();
+ }
+ }
+
+ async _readUntilSegmentAt(relativeTimestamp: number) {
+ while (!this._lineReader.reachedEnd) {
+ const lastSegment = last(this._segments);
+ if (lastSegment && lastSegment.relativeTimestamp > relativeTimestamp) {
+ break;
+ }
+
+ await this._readNextSegment();
+ }
+ }
+}
+
+const isM3u8Comment = (line: string) => line.startsWith('#') && !line.startsWith('#EXT');
+
+class AttributeList {
+ _attributes: Record = {};
+
+ constructor(str: string) {
+ let key = '';
+ let value = '';
+ let inValue = false;
+ let inQuotes = false;
+
+ for (let i = 0; i < str.length; i++) {
+ const char = str[i]!;
+
+ if (char === '"') {
+ inQuotes = !inQuotes;
+ } else if (char === '=' && !inValue && !inQuotes) {
+ inValue = true;
+ } else if (char === ',' && !inQuotes) {
+ if (key) {
+ this._attributes[key.toLowerCase()] = value;
+ }
+
+ key = '';
+ value = '';
+ inValue = false;
+ } else if (inValue) {
+ value += char;
+ } else {
+ key += char;
+ }
+ }
+
+ if (key) {
+ this._attributes[key.toLowerCase()] = value;
+ }
+ }
+
+ get(name: string) {
+ return this._attributes[name.toLowerCase()] ?? null;
+ }
+
+ getAsNumber(name: string) {
+ const value = this.get(name);
+ if (value === null) {
+ return null;
+ }
+
+ const num = Number(value);
+ return Number.isFinite(num) ? num : null;
+ }
+}
diff --git a/src/manifest-input-format.ts b/src/manifest-input-format.ts
new file mode 100644
index 0000000..da0e316
--- /dev/null
+++ b/src/manifest-input-format.ts
@@ -0,0 +1,35 @@
+/*!
+ * Copyright (c) 2026-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import { M3u8Parser } from './m3u8/m3u8-parser';
+import { ManifestInput } from './manifest-input';
+import { ManifestParser } from './manifest-parser';
+import { readAscii } from './reader';
+
+export abstract class ManifestInputFormat {
+ abstract _canReadManifestInput(input: ManifestInput): Promise;
+ abstract _createParser(input: ManifestInput): ManifestParser;
+}
+
+export class M3u8ManifestInputFormat extends ManifestInputFormat {
+ async _canReadManifestInput(input: ManifestInput) {
+ let slice = input._entryReader.requestSlice(0, 7);
+ if (slice instanceof Promise) slice = await slice;
+ if (!slice) return false;
+
+ return readAscii(slice, 7) === '#EXTM3U';
+ }
+
+ _createParser(input: ManifestInput) {
+ return new M3u8Parser(input);
+ }
+}
+
+export const M3U8 = /* #__PURE__ */ new M3u8ManifestInputFormat();
+
+export const ALL_MANIFEST_FORMATS = [M3U8];
diff --git a/src/manifest-input-segment.ts b/src/manifest-input-segment.ts
new file mode 100644
index 0000000..9a3b632
--- /dev/null
+++ b/src/manifest-input-segment.ts
@@ -0,0 +1,112 @@
+import { AES_128_BLOCK_SIZE, createAesDecryptStream } from './aes';
+import { Input } from './input';
+import { ManifestInputVariant } from './manifest-input-variant';
+import { assert } from './misc';
+import { fs } from './node';
+import { readBytes, Reader } from './reader';
+import { ReadableStreamSource, Source } from './source';
+
+export type SegmentEncryptionInfo = {
+ method: 'AES-128';
+ keyUri: string;
+ iv: Uint8Array | null;
+ keyFormat: string;
+};
+
+export type ManifestInputSegmentLocation = {
+ path: string;
+ offset: number;
+ length: number | null;
+};
+
+export class ManifestInputSegment {
+ readonly variant: ManifestInputVariant;
+ readonly location: ManifestInputSegmentLocation;
+ readonly relativeTimestamp: number;
+ readonly duration: number;
+ readonly title: string | null;
+ readonly encryption: SegmentEncryptionInfo | null;
+ readonly initSegment: ManifestInputSegment | null;
+
+ constructor(
+ variant: ManifestInputVariant,
+ location: ManifestInputSegmentLocation,
+ relativeTimestamp: number,
+ duration: number,
+ title: string | null,
+ encryption: SegmentEncryptionInfo | null,
+ initSegment: ManifestInputSegment | null,
+ ) {
+ this.variant = variant;
+ this.location = location;
+ this.relativeTimestamp = relativeTimestamp;
+ this.duration = duration;
+ this.title = title;
+ this.encryption = encryption;
+ this.initSegment = initSegment;
+ }
+
+ async toInput() {
+ let source: Source;
+
+ const needsSlice = this.location.offset > 0 || this.location.length !== null;
+
+ if (!this.encryption) {
+ source = await this.variant.input._getSourceCached(this.location.path);
+ if (needsSlice) {
+ source = source.slice(this.location.offset, this.location.length ?? undefined);
+ }
+ } else {
+ assert(this.encryption.iv);
+
+ let ciphertextSource = await this.variant.input._getSourceCached(this.location.path);
+ if (needsSlice) {
+ // Slice before decrypting
+ ciphertextSource = ciphertextSource.slice(this.location.offset, this.location.length ?? undefined);
+ }
+
+ const ciphertextReader = await Reader.fromSource(ciphertextSource);
+
+ const stream = createAesDecryptStream(ciphertextReader, async () => {
+ const keyReader = await this.variant.input._getEncryptionKeyReader(this.encryption!.keyUri);
+ const keySlice = await keyReader.requestSlice(0, AES_128_BLOCK_SIZE);
+ if (!keySlice) {
+ throw new Error('Invalid AES-128 key; expected at least 16 bytes of data.');
+ }
+ const key = readBytes(keySlice, AES_128_BLOCK_SIZE);
+
+ return { key, iv: this.encryption!.iv! };
+ });
+
+ /*
+ const chunks: Uint8Array[] = [];
+ const streamReader = stream.getReader();
+ while (true) {
+ const { done, value } = await streamReader.read();
+ if (done) {
+ break;
+ }
+
+ chunks.push(value);
+ }
+
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
+ const decryptedData = new Uint8Array(totalLength);
+ let offset = 0;
+ for (const chunk of chunks) {
+ decryptedData.set(chunk, offset);
+ offset += chunk.length;
+ }
+
+ await fs.writeFile('tempfile.ts', decryptedData);
+ */
+
+ source = new ReadableStreamSource(stream);
+ }
+
+ return new Input({
+ source,
+ formats: this.variant.input._mediaFormats,
+ });
+ }
+}
diff --git a/src/manifest-input-variant.ts b/src/manifest-input-variant.ts
new file mode 100644
index 0000000..d2820b8
--- /dev/null
+++ b/src/manifest-input-variant.ts
@@ -0,0 +1,457 @@
+/*!
+ * Copyright (c) 2026-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import { AudioCodec, MediaCodec, VideoCodec } from './codec';
+import { Demuxer } from './demuxer';
+import { Input } from './input';
+import { VirtualInputFormat } from './input-format';
+import {
+ InputAudioTrack,
+ InputAudioTrackBacking,
+ InputTrack,
+ InputTrackBacking,
+ InputVideoTrack,
+ InputVideoTrackBacking,
+} from './input-track';
+import { ManifestInput } from './manifest-input';
+import { ManifestInputSegment } from './manifest-input-segment';
+import { PacketRetrievalOptions } from './media-sink';
+import { MetadataTags, TrackDisposition } from './metadata';
+import { arrayArgmin, arrayCount, assert, Rotation } from './misc';
+import { EncodedPacket } from './packet';
+import { NullSource } from './source';
+
+export type ManifestInputVariantMetadata = {
+ name: string | null;
+ bitrate: number | null; // doc block: this refers to the _peak_ bitrate
+ averageBitrate: number | null;
+ codecs: MediaCodec[];
+ codecStrings: string[];
+ resolution: { width: number; height: number } | null;
+ frameRate: number | null;
+ isKeyFrameOnly: boolean;
+};
+
+export abstract class ManifestInputVariant {
+ readonly input: ManifestInput;
+ readonly path: string;
+
+ /** @internal */
+ constructor(input: ManifestInput, path: string) {
+ this.input = input;
+ this.path = path;
+ }
+
+ abstract get metadata(): ManifestInputVariantMetadata;
+ abstract get groupId(): string | null;
+ abstract get associatedGroupId(): string | null;
+
+ abstract getFirstSegment(): Promise;
+ abstract getSegmentAt(timestamp: number): Promise;
+ abstract getNextSegment(segment: ManifestInputSegment): Promise;
+ abstract getPreviousSegment(segment: ManifestInputSegment): Promise;
+
+ async* segments(startTimestamp?: number) {
+ let currentSegment: ManifestInputSegment | null;
+
+ if (startTimestamp !== undefined) {
+ currentSegment = await this.getSegmentAt(startTimestamp);
+ } else {
+ currentSegment = await this.getFirstSegment();
+ }
+
+ while (currentSegment !== null) {
+ yield currentSegment;
+ currentSegment = await this.getNextSegment(currentSegment);
+ }
+ }
+
+ toInput() {
+ return new Input({
+ source: new NullSource(),
+ formats: [new VirtualInputFormat(input => new ManifestInputVariantDemuxer(input, this))],
+ });
+ }
+}
+
+class ManifestInputVariantDemuxer extends Demuxer {
+ variant: ManifestInputVariant;
+ tracksPromise: Promise | null = null;
+ firstSegment: ManifestInputSegment | null = null;
+
+ nextInputCacheAge = 0;
+ inputCache: {
+ segment: ManifestInputSegment;
+ inputPromise: Promise; // We store the promise so it's immediately available in the cache
+ age: number;
+ }[] = [];
+
+ initSegmentFirstTimestamps = new WeakMap();
+
+ constructor(input: Input, variant: ManifestInputVariant) {
+ super(input);
+
+ this.variant = variant;
+ }
+
+ async computeDuration(): Promise {
+ const tracks = await this.getTracks();
+ const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
+ return Math.max(0, ...trackDurations);
+ }
+
+ async getMetadataTags(): Promise {
+ return {}; // todo?
+ }
+
+ async getMimeType(): Promise {
+ return ''; // todo?
+ }
+
+ async getTracks(): Promise {
+ return this.tracksPromise ??= (async () => {
+ this.firstSegment = await this.variant.getFirstSegment();
+ if (!this.firstSegment) {
+ return [];
+ }
+
+ const input = await this.retrieveInputForSegment(this.firstSegment);
+ const inputTracks = await input.getTracks();
+
+ const tracks: InputTrack[] = [];
+ for (const track of inputTracks) {
+ if (track.type === 'video') {
+ const number = arrayCount(tracks, x => x.type === 'video') + 1;
+
+ tracks.push(new InputVideoTrack(
+ this.input,
+ new ManifestInputVariantInputVideoTrackBacking(track, this, number),
+ ));
+ } else if (track.type === 'audio') {
+ const number = arrayCount(tracks, x => x.type === 'audio') + 1;
+
+ tracks.push(new InputAudioTrack(
+ this.input,
+ new ManifestInputVariantInputAudioTrackBacking(track, this, number),
+ ));
+ }
+ }
+
+ return tracks;
+ })();
+ }
+
+ retrieveInputForSegment(segment: ManifestInputSegment) {
+ const cacheEntry = this.inputCache.find(x => x.segment === segment);
+ if (cacheEntry) {
+ cacheEntry.age = this.nextInputCacheAge++;
+ return cacheEntry.inputPromise;
+ }
+
+ const promise = segment.toInput();
+ this.inputCache.push({
+ segment,
+ inputPromise: promise,
+ age: this.nextInputCacheAge++,
+ });
+
+ const MAX_INPUT_CACHE_SIZE = 4;
+ if (this.inputCache.length > MAX_INPUT_CACHE_SIZE) {
+ const minAgeIndex = arrayArgmin(this.inputCache, x => x.age);
+ this.inputCache.splice(minAgeIndex, 1);
+ }
+
+ return promise;
+ }
+
+ async getMediaOffset(segment: ManifestInputSegment, input: Input) {
+ const initSegment = segment.initSegment ?? segment;
+
+ let initSegmentFirstTimestamp: number;
+ if (this.initSegmentFirstTimestamps.has(initSegment)) {
+ initSegmentFirstTimestamp = this.initSegmentFirstTimestamps.get(initSegment)!;
+ } else {
+ const initInput = await this.retrieveInputForSegment(initSegment);
+ initSegmentFirstTimestamp = await initInput.getFirstTimestamp();
+ this.initSegmentFirstTimestamps.set(initSegment, initSegmentFirstTimestamp);
+ }
+
+ let mediaOffset = 0;
+ mediaOffset -= initSegmentFirstTimestamp; // Make the timestamps relative to the init segment first timestamp
+ mediaOffset += initSegment.relativeTimestamp; // And then offset them by init segment's relative timestamp
+
+ if (segment !== initSegment) {
+ const segmentFirstTimestamp = await input.getFirstTimestamp();
+ if (segmentFirstTimestamp === initSegmentFirstTimestamp) {
+ // Normally, we expect segments (belonging to the same init segment / discontinuity) to have the same
+ // timestamp "base", meaning timestamps tick up across segments and don't reset. However, some
+ // containers don't have absolute timestamps and always start at 0 (like ADTS or MP3) or those that do
+ // always start at the same timestamp, we still want to produce sensible results. So, we detect that by
+ // checking if the internal timestamp hasn't risen faster than the segment durations would dictate. And
+ // if so, we offset.
+ const timeSinceInitSegment = segment.relativeTimestamp - initSegment.relativeTimestamp;
+ mediaOffset += timeSinceInitSegment;
+ }
+ }
+
+ return mediaOffset;
+ }
+}
+
+type PacketInfo = {
+ segment: ManifestInputSegment;
+ track: InputTrack;
+ sourcePacket: EncodedPacket;
+};
+
+class ManifestInputVariantInputTrackBacking implements InputTrackBacking {
+ firstInputTrack: InputTrack;
+ demuxer: ManifestInputVariantDemuxer;
+ packetInfos = new WeakMap();
+ number: number;
+
+ constructor(firstInputTrack: InputTrack, demuxer: ManifestInputVariantDemuxer, number: number) {
+ this.firstInputTrack = firstInputTrack;
+ this.demuxer = demuxer;
+ this.number = number;
+ }
+
+ getId(): number {
+ return this.firstInputTrack._backing.getId();
+ }
+
+ getNumber(): number {
+ return this.number;
+ }
+
+ getCodec(): MediaCodec | null {
+ return this.firstInputTrack._backing.getCodec();
+ }
+
+ getInternalCodecId(): string | number | Uint8Array | null {
+ return this.firstInputTrack._backing.getInternalCodecId();
+ }
+
+ getDisposition(): TrackDisposition {
+ return this.firstInputTrack._backing.getDisposition();
+ }
+
+ getLanguageCode(): string {
+ return this.firstInputTrack._backing.getLanguageCode();
+ }
+
+ getName(): string | null {
+ return this.firstInputTrack._backing.getName();
+ }
+
+ getTimeResolution(): number {
+ return this.firstInputTrack._backing.getTimeResolution();
+ }
+
+ getVariant(): ManifestInputVariant | null {
+ return this.demuxer.variant;
+ }
+
+ async getFirstTimestamp(): Promise {
+ const firstPacket = await this.getFirstPacket({ metadataOnly: true });
+ return firstPacket?.timestamp ?? 0;
+ }
+
+ async computeDuration(): Promise {
+ const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
+ return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
+ }
+
+ async createAdjustedPacket(packet: EncodedPacket, segment: ManifestInputSegment, track: InputTrack) {
+ const mediaOffset = await this.demuxer.getMediaOffset(segment, track.input);
+
+ const modified = packet.clone({
+ timestamp: packet.timestamp + mediaOffset,
+ // The 1e8 assumes a max of 100 MB per second, highly unlikely to be hit, so this should guarantee
+ // monotonically increasing sequence numbers across segments.
+ sequenceNumber: Math.floor(1e8 * segment.relativeTimestamp) + packet.sequenceNumber,
+ });
+
+ this.packetInfos.set(modified, {
+ segment,
+ track,
+ sourcePacket: packet,
+ });
+
+ return modified;
+ }
+
+ async getFirstPacket(options: PacketRetrievalOptions): Promise {
+ assert(this.demuxer.firstSegment);
+
+ const packet = await this.firstInputTrack._backing.getFirstPacket(options);
+ if (!packet) {
+ return null;
+ }
+
+ return this.createAdjustedPacket(packet, this.demuxer.firstSegment, this.firstInputTrack);
+ }
+
+ getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
+ return this._getNextInternal(packet, options, false);
+ }
+
+ getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise {
+ return this._getNextInternal(packet, options, true);
+ }
+
+ async _getNextInternal(
+ packet: EncodedPacket,
+ options: PacketRetrievalOptions,
+ keyframesOnly: boolean,
+ ): Promise {
+ const info = this.packetInfos.get(packet);
+ if (!info) {
+ throw new Error('Packet was not created from this track.');
+ }
+
+ // console.log(info.segment.path);
+
+ const nextPacket = keyframesOnly
+ ? await info.track._backing.getNextKeyPacket(info.sourcePacket, options)
+ : await info.track._backing.getNextPacket(info.sourcePacket, options);
+ if (nextPacket) {
+ return this.createAdjustedPacket(nextPacket, info.segment, info.track);
+ }
+
+ let currentSegment: ManifestInputSegment | null = info.segment;
+ while (true) {
+ const nextSegment = await this.demuxer.variant.getNextSegment(currentSegment);
+ if (!nextSegment) {
+ return null;
+ }
+
+ const nextInput = await this.demuxer.retrieveInputForSegment(nextSegment);
+ const nextTracks = await nextInput.getTracks();
+ const nextTrack = nextTracks.find(t => t.type === info.track.type && t.number === info.track.number);
+
+ if (!nextTrack) {
+ currentSegment = nextSegment;
+ continue;
+ }
+
+ const firstPacket = await nextTrack._backing.getFirstPacket(options);
+ if (!firstPacket) {
+ return null;
+ }
+
+ return this.createAdjustedPacket(firstPacket, nextSegment, nextTrack);
+ }
+ }
+
+ getPacket(timestamp: number, options: PacketRetrievalOptions): Promise {
+ return this._getPacketInternal(timestamp, options, false);
+ }
+
+ getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise {
+ return this._getPacketInternal(timestamp, options, true);
+ }
+
+ async _getPacketInternal(
+ timestamp: number,
+ options: PacketRetrievalOptions,
+ keyframesOnly: boolean,
+ ): Promise {
+ let currentSegment = await this.demuxer.variant.getSegmentAt(timestamp);
+ if (!currentSegment) {
+ return null;
+ }
+
+ while (currentSegment) {
+ const input = await this.demuxer.retrieveInputForSegment(currentSegment);
+ const tracks = await input.getTracks();
+ const track = tracks.find(t => (
+ t.type === this.firstInputTrack.type && t.number === this.firstInputTrack.number
+ ));
+
+ if (!track) {
+ // Search the previous segment
+ currentSegment = await this.demuxer.variant.getPreviousSegment(currentSegment);
+ continue;
+ }
+
+ const mediaOffset = await this.demuxer.getMediaOffset(currentSegment, input);
+
+ const offsetTimestamp = timestamp - mediaOffset;
+ const packet = keyframesOnly
+ ? await track._backing.getKeyPacket(offsetTimestamp, options)
+ : await track._backing.getPacket(offsetTimestamp, options);
+
+ if (!packet) {
+ // Search the previous segment
+ currentSegment = await this.demuxer.variant.getPreviousSegment(currentSegment);
+ continue;
+ }
+
+ return this.createAdjustedPacket(packet, currentSegment, track);
+ }
+
+ return null;
+ }
+}
+
+class ManifestInputVariantInputVideoTrackBacking
+ extends ManifestInputVariantInputTrackBacking
+ implements InputVideoTrackBacking {
+ override firstInputTrack!: InputVideoTrack;
+
+ override getCodec(): VideoCodec | null {
+ return this.firstInputTrack._backing.getCodec();
+ }
+
+ getCodedWidth(): number {
+ return this.firstInputTrack._backing.getCodedWidth();
+ }
+
+ getCodedHeight(): number {
+ return this.firstInputTrack._backing.getCodedHeight();
+ }
+
+ getRotation(): Rotation {
+ return this.firstInputTrack._backing.getRotation();
+ }
+
+ getColorSpace(): Promise {
+ return this.firstInputTrack._backing.getColorSpace();
+ }
+
+ canBeTransparent(): Promise {
+ return this.firstInputTrack._backing.canBeTransparent();
+ }
+
+ getDecoderConfig(): Promise {
+ return this.firstInputTrack._backing.getDecoderConfig();
+ }
+}
+
+class ManifestInputVariantInputAudioTrackBacking
+ extends ManifestInputVariantInputTrackBacking
+ implements InputAudioTrackBacking {
+ override firstInputTrack!: InputAudioTrack;
+
+ override getCodec(): AudioCodec | null {
+ return this.firstInputTrack._backing.getCodec();
+ }
+
+ getNumberOfChannels(): number {
+ return this.firstInputTrack._backing.getNumberOfChannels();
+ }
+
+ getSampleRate(): number {
+ return this.firstInputTrack._backing.getSampleRate();
+ }
+
+ getDecoderConfig(): Promise {
+ return this.firstInputTrack._backing.getDecoderConfig();
+ }
+}
diff --git a/src/manifest-input.ts b/src/manifest-input.ts
new file mode 100644
index 0000000..23b1dab
--- /dev/null
+++ b/src/manifest-input.ts
@@ -0,0 +1,189 @@
+/*!
+ * Copyright (c) 2026-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import { Input } from './input';
+import { InputFormat, VirtualInputFormat } from './input-format';
+import { ManifestInputFormat } from './manifest-input-format';
+import { ManifestParser } from './manifest-parser';
+import { arrayArgmin, assert, MaybePromise, polyfillSymbolDispose } from './misc';
+import { Reader } from './reader';
+import { NullSource, Source } from './source';
+import { ManifestInputVariant } from './manifest-input-variant';
+import { InputAggregateDemuxer } from './aggregate-demuxer';
+
+polyfillSymbolDispose();
+
+export type ManifestInputOptions = {
+ entryPath: string;
+ getSource: (path: string) => MaybePromise;
+ manifestFormats: ManifestInputFormat[];
+ mediaFormats: InputFormat[];
+};
+
+export class ManifestInput implements Disposable {
+ _entryPath: string;
+ _getSourceUncached: (path: string) => MaybePromise;
+ _manifestFormats: ManifestInputFormat[];
+ _mediaFormats: InputFormat[];
+ _parserPromise: Promise | null = null;
+ _format: ManifestInputFormat | null = null;
+ _entryReader!: Reader;
+ _disposed = false;
+ _encryptionKeyReaders = new Map>();
+ _nextSourceCacheAge = 0;
+ _sourceCache: {
+ path: string;
+ sourcePromise: Promise;
+ age: number;
+ }[] = [];
+
+ get disposed() {
+ return this._disposed;
+ }
+
+ constructor(options: ManifestInputOptions) {
+ if (!options || typeof options !== 'object') {
+ throw new TypeError('options must be an object.');
+ }
+ if (typeof options.entryPath !== 'string') {
+ throw new TypeError('options.entryPath must be a string.');
+ }
+ if (typeof options.getSource !== 'function') {
+ throw new TypeError('options.getSource must be a function.');
+ }
+ if (
+ !Array.isArray(options.manifestFormats)
+ || options.manifestFormats.some(x => !(x instanceof ManifestInputFormat))
+ ) {
+ throw new TypeError('options.manifestFormats must be an array of ManifestInputFormat.');
+ }
+ if (
+ !Array.isArray(options.mediaFormats)
+ || options.mediaFormats.some(x => !(x instanceof InputFormat))
+ ) {
+ throw new TypeError('options.mediaFormats must be an array of InputFormat.');
+ }
+
+ this._entryPath = options.entryPath;
+ this._manifestFormats = options.manifestFormats;
+ this._mediaFormats = options.mediaFormats;
+
+ this._getSourceUncached = async (path) => {
+ const source = await options.getSource(path);
+ if (!(source instanceof Source)) {
+ throw new TypeError('The getSource function must return a Source.');
+ }
+ if (source._disposed) {
+ throw new TypeError('The Source returned by getSource must not be disposed.');
+ }
+
+ return source;
+ };
+ }
+
+ _getParser() {
+ return this._parserPromise ??= (async () => {
+ const entrySource = await this._getSourceUncached(this._entryPath);
+ this._entryReader = await Reader.fromSource(entrySource);
+
+ for (const format of this._manifestFormats) {
+ const canRead = await format._canReadManifestInput(this);
+ if (canRead) {
+ this._format = format;
+ return format._createParser(this);
+ }
+ }
+
+ throw new Error('Manifest input has an unsupported or unrecognizable format.');
+ })();
+ }
+
+ async getFormat() {
+ await this._getParser();
+ assert(this._format!);
+ return this._format;
+ }
+
+ async getVariants(): Promise {
+ const parser = await this._getParser();
+ const variants = await parser.getVariants();
+
+ const sorted = [...variants]
+ .sort((a, b) => {
+ // Variants with unknown bitrate come last
+ return (b.metadata.bitrate ?? b.metadata.averageBitrate ?? -Infinity)
+ - (a.metadata.bitrate ?? a.metadata.averageBitrate ?? -Infinity);
+ });
+
+ return sorted;
+ }
+
+ async getPrimaryVariant() {
+ const variants = await this.getVariants();
+ return variants[0] ?? null;
+ }
+
+ async toInput() {
+ // Create one "mega input" that contains all tracks from all variants
+ const variants = await this.getVariants();
+ const subInputs = variants.map(v => v.toInput());
+
+ return new Input({
+ source: new NullSource(),
+ formats: [new VirtualInputFormat(input => new InputAggregateDemuxer(input, subInputs))],
+ });
+ }
+
+ _getSourceCached(path: string) {
+ const cachedEntry = this._sourceCache.find(x => x.path === path);
+ if (cachedEntry) {
+ cachedEntry.age++;
+ return cachedEntry.sourcePromise;
+ }
+
+ const sourcePromise = Promise.resolve(this._getSourceUncached(path));
+ this._sourceCache.push({
+ path,
+ sourcePromise,
+ age: this._nextSourceCacheAge++,
+ });
+
+ const MAX_SOURCE_CACHE_SIZE = 4;
+ if (this._sourceCache.length > MAX_SOURCE_CACHE_SIZE) {
+ const minAgeIndex = arrayArgmin(this._sourceCache, x => x.age);
+ this._sourceCache.splice(minAgeIndex, 1);
+ }
+
+ return sourcePromise;
+ }
+
+ _getEncryptionKeyReader(path: string) {
+ let cachedEntry = this._encryptionKeyReaders.get(path);
+ if (cachedEntry) {
+ return cachedEntry;
+ }
+
+ cachedEntry = Promise.resolve(this._getSourceUncached(path))
+ .then(keySource => Reader.fromSource(keySource));
+ this._encryptionKeyReaders.set(path, cachedEntry);
+
+ return cachedEntry;
+ }
+
+ dispose() {
+ if (this._disposed) {
+ return;
+ }
+
+ this._disposed = true;
+ }
+
+ [Symbol.dispose]() {
+ this.dispose();
+ }
+}
diff --git a/src/manifest-parser.ts b/src/manifest-parser.ts
new file mode 100644
index 0000000..c72f148
--- /dev/null
+++ b/src/manifest-parser.ts
@@ -0,0 +1,22 @@
+/*!
+ * Copyright (c) 2026-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+import { ManifestInput } from './manifest-input';
+import { ManifestInputVariant } from './manifest-input-variant';
+
+export class ManifestParser {
+ _input: ManifestInput;
+
+ constructor(input: ManifestInput) {
+ this._input = input;
+ }
+
+ getVariants(): Promise {
+ throw new Error('Not implemented.');
+ }
+}
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index f9d8a39..749f0db 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -1906,6 +1906,10 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
return firstPacket?.timestamp ?? 0;
}
+ getVariant() {
+ return null;
+ }
+
getTimeResolution() {
return this.internalTrack.segment.timestampFactor;
}
diff --git a/src/misc.ts b/src/misc.ts
index eaefa9d..4dc150a 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -850,3 +850,84 @@ export const polyfillSymbolDispose = () => {
export const isNumber = (x: unknown) => {
return typeof x === 'number' && !Number.isNaN(x);
};
+
+export const joinPaths = (basePath: string, relativePath: string) => {
+ // If relativePath is a full URL with protocol, return it as-is
+ if (relativePath.includes('://')) {
+ return relativePath;
+ }
+
+ let result: string;
+
+ if (relativePath.startsWith('/')) {
+ const protocolIndex = basePath.indexOf('://');
+ if (protocolIndex === -1) {
+ result = relativePath;
+ } else {
+ const pathStart = basePath.indexOf('/', protocolIndex + 3);
+ if (pathStart === -1) {
+ result = basePath + relativePath;
+ } else {
+ result = basePath.slice(0, pathStart) + relativePath;
+ }
+ }
+ } else {
+ const lastSlash = basePath.lastIndexOf('/');
+ if (lastSlash === -1) {
+ result = relativePath;
+ } else {
+ result = basePath.slice(0, lastSlash + 1) + relativePath;
+ }
+ }
+
+ // Normalize ./ and ../
+
+ let prefix = '';
+ const protocolIndex = result.indexOf('://');
+ if (protocolIndex !== -1) {
+ const pathStart = result.indexOf('/', protocolIndex + 3);
+ if (pathStart !== -1) {
+ prefix = result.slice(0, pathStart);
+ result = result.slice(pathStart);
+ }
+ }
+
+ const segments = result.split('/');
+ const normalized: string[] = [];
+ for (const segment of segments) {
+ if (segment === '..') {
+ normalized.pop();
+ } else if (segment !== '.') {
+ normalized.push(segment);
+ }
+ }
+
+ return prefix + normalized.join('/');
+};
+
+export const arrayCount = (array: T[], predicate: (item: T) => boolean) => {
+ let count = 0;
+
+ for (let i = 0; i < array.length; i++) {
+ if (predicate(array[i]!)) {
+ count++;
+ }
+ }
+
+ return count;
+};
+
+export const arrayArgmin = (array: T[], getValue: (item: T) => number): number => {
+ let minIndex = -1;
+ let minValue = Infinity;
+
+ for (let i = 0; i < array.length; i++) {
+ const value = getValue(array[i]!);
+ if (value < minValue) {
+ minValue = value;
+ minIndex = i;
+ }
+ }
+
+ return minIndex;
+};
diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts
index 12ada99..cb9dd74 100644
--- a/src/mp3/mp3-demuxer.ts
+++ b/src/mp3/mp3-demuxer.ts
@@ -267,6 +267,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
};
}
+ getVariant() {
+ return null;
+ }
+
async getDecoderConfig(): Promise {
assert(this.demuxer.firstFrameHeader);
diff --git a/src/mp3/mp3-reader.ts b/src/mp3/mp3-reader.ts
index 8b323e2..0226fdf 100644
--- a/src/mp3/mp3-reader.ts
+++ b/src/mp3/mp3-reader.ts
@@ -15,6 +15,8 @@ export const readNextMp3FrameHeader = async (reader: Reader, startPos: number, u
} | null> => {
let currentPos = startPos;
+ // todo optimize this shit wtf is this
+
while (until === null || currentPos < until) {
let slice = reader.requestSlice(currentPos, FRAME_HEADER_SIZE);
if (slice instanceof Promise) slice = await slice;
diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts
index 82fee7b..63bbd50 100644
--- a/src/mpeg-ts/mpeg-ts-demuxer.ts
+++ b/src/mpeg-ts/mpeg-ts-demuxer.ts
@@ -842,6 +842,10 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
return TIMESCALE;
}
+ getVariant() {
+ return null;
+ }
+
async computeDuration(): Promise {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts
index fa364d4..de2606d 100644
--- a/src/ogg/ogg-demuxer.ts
+++ b/src/ogg/ogg-demuxer.ts
@@ -458,6 +458,10 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
return null;
}
+ getVariant() {
+ return null;
+ }
+
async getDecoderConfig(): Promise {
assert(this.bitstream.codecInfo.codec);
diff --git a/src/reader.ts b/src/reader.ts
index 4472fa7..824770d 100644
--- a/src/reader.ts
+++ b/src/reader.ts
@@ -13,7 +13,14 @@ import { Source } from './source';
export class Reader {
fileSize!: number | null;
- constructor(public source: Source) {}
+ private constructor(public source: Source) {}
+
+ static async fromSource(source: Source) {
+ const reader = new this(source);
+ reader.fileSize = await source.getSizeOrNull();
+
+ return reader;
+ }
requestSlice(start: number, length: number): MaybePromise {
if (this.source._disposed) {
@@ -329,3 +336,78 @@ export const readAscii = (slice: FileSlice, length: number) => {
return str;
};
+
+export class LineReader {
+ getReader: () => MaybePromise;
+ isComment?: (line: string) => boolean;
+ reader: Reader | null = null;
+ textDecoder = new TextDecoder();
+ readPos = 0;
+ reachedEnd = false;
+ lineBuffer = '';
+
+ constructor(getReader: () => MaybePromise, isComment?: (line: string) => boolean) {
+ this.getReader = getReader;
+ this.isComment = isComment;
+ }
+
+ readNextLine(): MaybePromise {
+ if (this.reachedEnd) {
+ return null;
+ }
+
+ const line = this.extractLineFromBuffer();
+ if (line !== null) {
+ return line;
+ }
+
+ return (async () => {
+ if (!this.reader) {
+ let reader = this.getReader();
+ if (reader instanceof Promise) reader = await reader;
+
+ this.reader = reader;
+ }
+
+ while (true) {
+ let slice = this.reader.requestSliceRange(this.readPos, 0, 1024);
+ if (slice instanceof Promise) slice = await slice;
+
+ if (!slice || slice.length === 0) {
+ this.reachedEnd = true;
+ return null;
+ }
+
+ const bytes = readBytes(slice, slice.length);
+ this.readPos += bytes.length;
+
+ this.lineBuffer += this.textDecoder.decode(bytes, { stream: true });
+
+ const line = this.extractLineFromBuffer();
+ if (line !== null) {
+ return line;
+ }
+ }
+ })();
+ }
+
+ extractLineFromBuffer() {
+ assert(!this.reachedEnd);
+
+ while (true) {
+ const newlineIndex = this.lineBuffer.indexOf('\n');
+ if (newlineIndex === -1) {
+ return null;
+ }
+
+ const line = this.lineBuffer.slice(0, newlineIndex).trim();
+ this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
+
+ if (this.isComment?.(line)) {
+ continue;
+ }
+
+ return line;
+ }
+ }
+}
diff --git a/src/source.ts b/src/source.ts
index f7aa0df..07de065 100644
--- a/src/source.ts
+++ b/src/source.ts
@@ -10,6 +10,7 @@ import type { FileHandle } from 'node:fs/promises';
import {
assert,
binarySearchLessOrEqual,
+ clamp,
closedIntervalsOverlap,
isNumber,
isWebKit,
@@ -85,6 +86,17 @@ export abstract class Source {
return result;
}
+ slice(offset: number, length?: number) {
+ if (!Number.isInteger(offset) || offset < 0) {
+ throw new TypeError('offset must be a non-negative integer.');
+ }
+ if (length !== undefined && (!Number.isInteger(length) || length < 0)) {
+ throw new TypeError('length, when provided, must be a non-negative integer.');
+ }
+
+ return new RangedSource(this, offset, length);
+ }
+
/** Called each time data is retrieved from the source. Will be called with the retrieved range (end exclusive). */
onread: ((start: number, end: number) => unknown) | null = null;
}
@@ -1697,3 +1709,78 @@ class ReadOrchestrator {
this.disposed = true;
}
}
+
+/**
+ * A dummy source from which no data can be read. Can be used in conjunction with input formats that get their data
+ * from another source.
+ */
+export class NullSource extends Source {
+ override _retrieveSize(): MaybePromise {
+ return null;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ override _read(start: number, end: number): MaybePromise {
+ return null;
+ }
+
+ override _dispose(): void {
+ // Do nothing
+ }
+}
+
+export class RangedSource extends Source {
+ /** @internal */
+ _baseSource: Source;
+ /** @internal */
+ _offset: number;
+ /** @internal */
+ _length: number | null;
+
+ constructor(baseSource: Source, offset: number, length?: number) {
+ super();
+
+ this._baseSource = baseSource;
+ this._offset = offset;
+ this._length = length ?? null;
+ }
+
+ override async _retrieveSize(): Promise {
+ const baseSize = await this._baseSource.getSizeOrNull(); // Call getSizeOrNull for memoization
+ if (baseSize === null) {
+ return null;
+ }
+
+ return clamp(baseSize - this._offset, 0, this._length ?? Infinity);
+ }
+
+ override _read(start: number, end: number): MaybePromise {
+ if (this._length !== null && end > this._length) {
+ return null;
+ }
+
+ const result = this._baseSource._read(this._offset + start, this._offset + end);
+
+ if (result instanceof Promise) {
+ return result.then((result) => {
+ if (!result) {
+ return null;
+ }
+
+ result.offset -= this._offset;
+ return result;
+ });
+ } else {
+ if (!result) {
+ return null;
+ }
+
+ result.offset -= this._offset;
+ return result;
+ }
+ }
+
+ override _dispose(): void {
+ this._baseSource._dispose();
+ }
+}
diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts
index 8623963..a88ddb0 100644
--- a/src/wave/wave-demuxer.ts
+++ b/src/wave/wave-demuxer.ts
@@ -407,6 +407,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
return null;
}
+ getVariant() {
+ return null;
+ }
+
getLanguageCode() {
return UNDETERMINED_LANGUAGE;
}
diff --git a/test/node/aes.test.ts b/test/node/aes.test.ts
new file mode 100644
index 0000000..5afbd08
--- /dev/null
+++ b/test/node/aes.test.ts
@@ -0,0 +1,62 @@
+import { expect, test } from 'vitest';
+import { Reader } from '../../src/reader.js';
+import { BufferSource } from '../../src/source.js';
+import { createAesDecryptStream } from '../../src/aes.js';
+
+// getRandomValues is length-limited, so let's just do this
+export const fillRandom = (buffer: T) => {
+ for (let i = 0; i < buffer.length; i++) {
+ buffer[i] = Math.floor(Math.random() * 256);
+ }
+
+ return buffer;
+};
+
+test('createAesDecryptStream', async () => {
+ // Test for all paddings
+ for (let i = 0; i < 16; i++) {
+ const plaintextLength = Math.floor(Math.random() * 2 ** 18) + i;
+
+ const plaintext = fillRandom(new Uint8Array(plaintextLength));
+ const key = fillRandom(new Uint8Array(16));
+ const iv = fillRandom(new Uint8Array(16));
+
+ const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']);
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext));
+
+ const source = new BufferSource(ciphertext);
+ const reader = await Reader.fromSource(source);
+
+ const stream = createAesDecryptStream(reader, () => ({ key, iv }));
+ const streamReader = stream.getReader();
+
+ const chunks: Uint8Array[] = [];
+ while (true) {
+ const { done, value } = await streamReader.read();
+ if (done) {
+ break;
+ }
+
+ chunks.push(value);
+ }
+
+ // Concatenate chunks
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
+ const decrypted = new Uint8Array(totalLength);
+
+ let offset = 0;
+ for (const chunk of chunks) {
+ decrypted.set(chunk, offset);
+ offset += chunk.length;
+ }
+
+ expect(decrypted.length).toBe(plaintext.length);
+
+ // .toEqual is slow, so we do this instead
+ for (let j = 0; j < plaintext.length; j++) {
+ if (decrypted[j] !== plaintext[j]) {
+ throw new Error(`Mismatch at byte ${j} for padding ${16 - (i % 16)}`);
+ }
+ }
+ }
+});
diff --git a/test/node/join-paths.test.ts b/test/node/join-paths.test.ts
new file mode 100644
index 0000000..946d121
--- /dev/null
+++ b/test/node/join-paths.test.ts
@@ -0,0 +1,46 @@
+import { expect, test } from 'vitest';
+import { joinPaths } from '../../src/misc.js';
+
+test('joinPaths handles all path combinations correctly', () => {
+ // Simple relative paths
+ expect(joinPaths('path/to/entry.m3u8', 'other.m3u8')).toBe('path/to/other.m3u8');
+ expect(joinPaths('entry.m3u8', 'other.m3u8')).toBe('other.m3u8');
+ expect(joinPaths('/path/to/entry.m3u8', 'other.m3u8')).toBe('/path/to/other.m3u8');
+
+ // Absolute relative paths (starting with /)
+ expect(joinPaths('path/to/entry.m3u8', '/other.m3u8')).toBe('/other.m3u8');
+ expect(joinPaths('/path/to/entry.m3u8', '/other.m3u8')).toBe('/other.m3u8');
+
+ // With protocols
+ expect(joinPaths('https://example.com/path/to/entry.m3u8', 'other.m3u8'))
+ .toBe('https://example.com/path/to/other.m3u8');
+ expect(joinPaths('https://example.com/path/to/entry.m3u8', '/other.m3u8'))
+ .toBe('https://example.com/other.m3u8');
+ expect(joinPaths('file:///path/to/entry.m3u8', 'other.m3u8')).toBe('file:///path/to/other.m3u8');
+ expect(joinPaths('file:///path/to/entry.m3u8', '/other.m3u8')).toBe('file:///other.m3u8');
+
+ // With ./
+ expect(joinPaths('path/to/entry.m3u8', './other.m3u8')).toBe('path/to/other.m3u8');
+ expect(joinPaths('https://example.com/path/to/entry.m3u8', './other.m3u8'))
+ .toBe('https://example.com/path/to/other.m3u8');
+
+ // With ../
+ expect(joinPaths('path/to/entry.m3u8', '../other.m3u8')).toBe('path/other.m3u8');
+ expect(joinPaths('path/to/deep/entry.m3u8', '../../other.m3u8')).toBe('path/other.m3u8');
+ expect(joinPaths('https://example.com/path/to/entry.m3u8', '../other.m3u8'))
+ .toBe('https://example.com/path/other.m3u8');
+ expect(joinPaths('https://example.com/a/b/c/entry.m3u8', '../../other.m3u8'))
+ .toBe('https://example.com/a/other.m3u8');
+
+ // Mixed ./ and ../
+ expect(joinPaths('path/to/entry.m3u8', './../other.m3u8')).toBe('path/other.m3u8');
+ expect(joinPaths('path/to/entry.m3u8', '../foo/../other.m3u8')).toBe('path/other.m3u8');
+
+ // Second argument is a full URL with protocol
+ expect(joinPaths('path/to/entry.m3u8', 'https://example.com/other.m3u8'))
+ .toBe('https://example.com/other.m3u8');
+ expect(joinPaths('https://example.com/path/to/entry.m3u8', 'https://other.com/other.m3u8'))
+ .toBe('https://other.com/other.m3u8');
+ expect(joinPaths('file:///path/to/entry.m3u8', 'https://example.com/other.m3u8'))
+ .toBe('https://example.com/other.m3u8');
+});
diff --git a/test/node/m3u8-input.test.ts b/test/node/m3u8-input.test.ts
new file mode 100644
index 0000000..03333c3
--- /dev/null
+++ b/test/node/m3u8-input.test.ts
@@ -0,0 +1,119 @@
+import { test } from 'vitest';
+import { ManifestInput } from '../../src/manifest-input.js';
+import { UrlSource } from '../../src/source.js';
+import { ALL_MANIFEST_FORMATS } from '../../src/manifest-input-format.js';
+import { ALL_FORMATS, MPEG_TS } from '../../src/input-format.js';
+import { EncodedPacketSink } from '../../src/media-sink.js';
+import { assert } from '../../src/misc.js';
+import { Input } from '../../src/input.js';
+
+test('yo', { timeout: 60_000 }, async () => {
+ /*
+ const yo = new Input({
+ source: new UrlSource('https://test-streams.mux.dev/x36xhzz/url_0/url_462/193039199_mp4_h264_aac_hd_7.ts'),
+ formats: ALL_FORMATS,
+ });
+
+ const videoTrack = await yo.getPrimaryVideoTrack();
+ const sink1 = new EncodedPacketSink(videoTrack!);
+
+ console.log(sink1.getFirstPacket());
+
+ for await (const packet of sink1.packets()) {
+ console.log(performance.now(), packet.timestamp);
+ }
+
+ console.log('Don');
+
+ return;
+
+ */
+ const manifest = new ManifestInput({
+ entryPath: 'https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8'
+ ?? 'https://playertest.longtailvideo.com/adaptive/customIV/prog_index.m3u8'
+ ?? 'https://playertest.longtailvideo.com/adaptive/issue666/playlists/cisq0gim60007xzvi505emlxx.m3u8'
+ ?? 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8'
+ ?? 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8'
+ ?? 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8'
+ ?? 'https://cdn.jwplayer.com/manifests/pZxWPRg4.m3u8'
+ ?? 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'
+ ?? 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8',
+ getSource: path => new UrlSource(path),
+ manifestFormats: ALL_MANIFEST_FORMATS,
+ mediaFormats: [MPEG_TS],
+ });
+
+ const variants = await manifest.getVariants();
+ const variant = variants[1]!;
+ const segment = await variant.getFirstSegment();
+ const thing = await segment!.toInput();
+ console.log(await thing.getTracks());
+ /*
+
+ const primaryVariant = (await manifest.getPrimaryVariant())!;
+ const segment = (await primaryVariant.getFirstSegment())!;
+ const thing = await segment.toInput();
+ console.log(await thing.getTracks());
+ */
+
+ return;
+
+ for await (const segment of primaryVariant.segments()) {
+ console.log(segment.relativeTimestamp, segment.initSegment?.relativeTimestamp);
+ }
+
+ return;
+
+ const input = await manifest.toInput();
+ const track = await input.getPrimaryVideoTrack();
+ const sink = new EncodedPacketSink(track!);
+
+ for await (const packet of sink.packets()) {
+ console.log(packet.timestamp);
+ }
+
+ return;
+
+ // const primaryVariant = (await manifest.getPrimaryVariant())!;
+
+ for await (const segment of primaryVariant.segments()) {
+ console.log(segment.location);
+ }
+
+ /*
+ for await (const segment of primaryVariant.segments()) {
+ const input = await segment.toInput();
+ console.log(segment.path, segment.discontinuity, await input.getFirstTimestamp());
+ continue;
+
+ if (segment.path.endsWith('u-6400-m-720x408-1628-a-96-1-1.ts')) {
+ const input = await segment.toInput();
+ const track = (await input.getPrimaryVideoTrack())!;
+ const timestamp = await track.getFirstTimestamp();
+ console.log(timestamp, await track.getDecoderConfig());
+
+ break;
+ }
+ }
+ */
+
+ /*
+ const segment = await primaryVariant.getFirstSegment();
+
+ // console.log(segment);
+
+ const input = primaryVariant.toInput();
+ // console.log(input);
+
+ // const tracks = await input.getTracks();
+ const track = await input.getPrimaryVideoTrack();
+
+ const sink = new EncodedPacketSink(track!);
+ let currentPacket = await sink.getFirstPacket();
+
+ while (currentPacket) {
+ console.log(currentPacket.timestamp);
+ currentPacket = await sink.getNextPacket(currentPacket);
+ }
+ */
+});
diff --git a/testfiles_temp/720p.m3u8 b/testfiles_temp/720p.m3u8
new file mode 100644
index 0000000..d3c1b37
--- /dev/null
+++ b/testfiles_temp/720p.m3u8
@@ -0,0 +1,133 @@
+#EXTM3U
+#EXT-X-VERSION:3
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-TARGETDURATION:11
+#EXTINF:10.000,
+url_462/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_463/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_464/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_465/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_466/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_467/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_468/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:9.950,
+url_469/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.050,
+url_470/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_471/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_472/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_473/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_474/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_475/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_476/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_477/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_478/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_479/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_480/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_481/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_482/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_483/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_484/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_485/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_486/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_487/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_488/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_489/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_490/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:9.950,
+url_491/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.050,
+url_492/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_493/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_494/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_495/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_496/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_497/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_498/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_499/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_500/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_501/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_502/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_503/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_504/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:9.950,
+url_505/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.050,
+url_506/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_507/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_508/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_509/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_510/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_511/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_512/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_513/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_514/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_515/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_516/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_517/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_518/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_519/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_520/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_521/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_522/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_523/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:10.000,
+url_524/193039199_mp4_h264_aac_hd_7.ts
+#EXTINF:4.584,
+url_525/193039199_mp4_h264_aac_hd_7.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry.m3u8 b/testfiles_temp/entry.m3u8
new file mode 100644
index 0000000..98b3ca5
--- /dev/null
+++ b/testfiles_temp/entry.m3u8
@@ -0,0 +1,11 @@
+#EXTM3U
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
+url_0/193039199_mp4_h264_aac_hd_7.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=246440,CODECS="mp4a.40.5,avc1.42000d",RESOLUTION=320x184,NAME="240"
+url_2/193039199_mp4_h264_aac_ld_7.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=460560,CODECS="mp4a.40.5,avc1.420016",RESOLUTION=512x288,NAME="380"
+url_4/193039199_mp4_h264_aac_7.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x480,NAME="480"
+url_6/193039199_mp4_h264_aac_hq_7.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6221600,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,NAME="1080"
+url_8/193039199_mp4_h264_aac_fhd_7.m3u8
diff --git a/testfiles_temp/entry2-variant1-seg1-packets.json b/testfiles_temp/entry2-variant1-seg1-packets.json
new file mode 100644
index 0000000..fd0d079
--- /dev/null
+++ b/testfiles_temp/entry2-variant1-seg1-packets.json
@@ -0,0 +1,2278 @@
+{
+ "packets": [
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 900000,
+ "pts_time": "10.000000",
+ "dts": 900000,
+ "dts_time": "10.000000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "34082",
+ "pos": "1128",
+ "flags": "K__",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 912000,
+ "pts_time": "10.133333",
+ "dts": 903000,
+ "dts_time": "10.033333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9097",
+ "pos": "36848",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 906000,
+ "pts_time": "10.066667",
+ "dts": 906000,
+ "dts_time": "10.066667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1860",
+ "pos": "46624",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 903000,
+ "pts_time": "10.033333",
+ "dts": 909000,
+ "dts_time": "10.100000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "536",
+ "pos": "49632",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 909000,
+ "pts_time": "10.100000",
+ "dts": 912000,
+ "dts_time": "10.133333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "653",
+ "pos": "50760",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 924000,
+ "pts_time": "10.266667",
+ "dts": 915000,
+ "dts_time": "10.166667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9177",
+ "pos": "52452",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 918000,
+ "pts_time": "10.200000",
+ "dts": 918000,
+ "dts_time": "10.200000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "2279",
+ "pos": "62416",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 915000,
+ "pts_time": "10.166667",
+ "dts": 921000,
+ "dts_time": "10.233333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "632",
+ "pos": "65424",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 921000,
+ "pts_time": "10.233333",
+ "dts": 924000,
+ "dts_time": "10.266667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "708",
+ "pos": "66552",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 936000,
+ "pts_time": "10.400000",
+ "dts": 927000,
+ "dts_time": "10.300000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9790",
+ "pos": "67680",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 930000,
+ "pts_time": "10.333333",
+ "dts": 930000,
+ "dts_time": "10.333333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "2355",
+ "pos": "78584",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 927000,
+ "pts_time": "10.300000",
+ "dts": 933000,
+ "dts_time": "10.366667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "788",
+ "pos": "81404",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 933000,
+ "pts_time": "10.366667",
+ "dts": 936000,
+ "dts_time": "10.400000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "958",
+ "pos": "83096",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 948000,
+ "pts_time": "10.533333",
+ "dts": 939000,
+ "dts_time": "10.433333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9425",
+ "pos": "84412",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 942000,
+ "pts_time": "10.466667",
+ "dts": 942000,
+ "dts_time": "10.466667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1907",
+ "pos": "94940",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 939000,
+ "pts_time": "10.433333",
+ "dts": 945000,
+ "dts_time": "10.500000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "763",
+ "pos": "97384",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 945000,
+ "pts_time": "10.500000",
+ "dts": 948000,
+ "dts_time": "10.533333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "673",
+ "pos": "98512",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 960000,
+ "pts_time": "10.666667",
+ "dts": 951000,
+ "dts_time": "10.566667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7981",
+ "pos": "100016",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 954000,
+ "pts_time": "10.600000",
+ "dts": 954000,
+ "dts_time": "10.600000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1776",
+ "pos": "108664",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 951000,
+ "pts_time": "10.566667",
+ "dts": 957000,
+ "dts_time": "10.633333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "617",
+ "pos": "111108",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 957000,
+ "pts_time": "10.633333",
+ "dts": 960000,
+ "dts_time": "10.666667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "635",
+ "pos": "112236",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 972000,
+ "pts_time": "10.800000",
+ "dts": 963000,
+ "dts_time": "10.700000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7111",
+ "pos": "113740",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 966000,
+ "pts_time": "10.733333",
+ "dts": 966000,
+ "dts_time": "10.733333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1444",
+ "pos": "121448",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 963000,
+ "pts_time": "10.700000",
+ "dts": 969000,
+ "dts_time": "10.766667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "580",
+ "pos": "123704",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 969000,
+ "pts_time": "10.766667",
+ "dts": 972000,
+ "dts_time": "10.800000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "455",
+ "pos": "124832",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 984000,
+ "pts_time": "10.933333",
+ "dts": 975000,
+ "dts_time": "10.833333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "6780",
+ "pos": "125772",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 978000,
+ "pts_time": "10.866667",
+ "dts": 978000,
+ "dts_time": "10.866667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1323",
+ "pos": "133292",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 975000,
+ "pts_time": "10.833333",
+ "dts": 981000,
+ "dts_time": "10.900000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "437",
+ "pos": "135172",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 981000,
+ "pts_time": "10.900000",
+ "dts": 984000,
+ "dts_time": "10.933333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "525",
+ "pos": "136300",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 996000,
+ "pts_time": "11.066667",
+ "dts": 987000,
+ "dts_time": "10.966667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8236",
+ "pos": "137428",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 990000,
+ "pts_time": "11.000000",
+ "dts": 990000,
+ "dts_time": "11.000000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1848",
+ "pos": "146640",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 987000,
+ "pts_time": "10.966667",
+ "dts": 993000,
+ "dts_time": "11.033333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "620",
+ "pos": "149084",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 993000,
+ "pts_time": "11.033333",
+ "dts": 996000,
+ "dts_time": "11.066667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "548",
+ "pos": "150212",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1008000,
+ "pts_time": "11.200000",
+ "dts": 999000,
+ "dts_time": "11.100000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8127",
+ "pos": "151528",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1002000,
+ "pts_time": "11.133333",
+ "dts": 1002000,
+ "dts_time": "11.133333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1368",
+ "pos": "160364",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 999000,
+ "pts_time": "11.100000",
+ "dts": 1005000,
+ "dts_time": "11.166667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "465",
+ "pos": "162620",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1005000,
+ "pts_time": "11.166667",
+ "dts": 1008000,
+ "dts_time": "11.200000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "386",
+ "pos": "163560",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1020000,
+ "pts_time": "11.333333",
+ "dts": 1011000,
+ "dts_time": "11.233333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8151",
+ "pos": "164876",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1014000,
+ "pts_time": "11.266667",
+ "dts": 1014000,
+ "dts_time": "11.266667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1535",
+ "pos": "173524",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1011000,
+ "pts_time": "11.233333",
+ "dts": 1017000,
+ "dts_time": "11.300000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "453",
+ "pos": "175592",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1017000,
+ "pts_time": "11.300000",
+ "dts": 1020000,
+ "dts_time": "11.333333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "621",
+ "pos": "176908",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1032000,
+ "pts_time": "11.466667",
+ "dts": 1023000,
+ "dts_time": "11.366667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9826",
+ "pos": "178036",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1026000,
+ "pts_time": "11.400000",
+ "dts": 1026000,
+ "dts_time": "11.400000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1937",
+ "pos": "188940",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1023000,
+ "pts_time": "11.366667",
+ "dts": 1029000,
+ "dts_time": "11.433333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "638",
+ "pos": "191384",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1029000,
+ "pts_time": "11.433333",
+ "dts": 1032000,
+ "dts_time": "11.466667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "582",
+ "pos": "192888",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1044000,
+ "pts_time": "11.600000",
+ "dts": 1035000,
+ "dts_time": "11.500000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7923",
+ "pos": "193828",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1038000,
+ "pts_time": "11.533333",
+ "dts": 1038000,
+ "dts_time": "11.533333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1431",
+ "pos": "202852",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1035000,
+ "pts_time": "11.500000",
+ "dts": 1041000,
+ "dts_time": "11.566667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "522",
+ "pos": "204732",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1041000,
+ "pts_time": "11.566667",
+ "dts": 1044000,
+ "dts_time": "11.600000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "364",
+ "pos": "205672",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1056000,
+ "pts_time": "11.733333",
+ "dts": 1047000,
+ "dts_time": "11.633333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7166",
+ "pos": "206988",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1050000,
+ "pts_time": "11.666667",
+ "dts": 1050000,
+ "dts_time": "11.666667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1565",
+ "pos": "214884",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1047000,
+ "pts_time": "11.633333",
+ "dts": 1053000,
+ "dts_time": "11.700000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "482",
+ "pos": "217140",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1053000,
+ "pts_time": "11.700000",
+ "dts": 1056000,
+ "dts_time": "11.733333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "518",
+ "pos": "218080",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1068000,
+ "pts_time": "11.866667",
+ "dts": 1059000,
+ "dts_time": "11.766667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8373",
+ "pos": "219396",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1062000,
+ "pts_time": "11.800000",
+ "dts": 1062000,
+ "dts_time": "11.800000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1828",
+ "pos": "228420",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1059000,
+ "pts_time": "11.766667",
+ "dts": 1065000,
+ "dts_time": "11.833333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "615",
+ "pos": "230864",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1065000,
+ "pts_time": "11.833333",
+ "dts": 1068000,
+ "dts_time": "11.866667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "639",
+ "pos": "232368",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1080000,
+ "pts_time": "12.000000",
+ "dts": 1071000,
+ "dts_time": "11.900000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9399",
+ "pos": "233496",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1074000,
+ "pts_time": "11.933333",
+ "dts": 1074000,
+ "dts_time": "11.933333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1692",
+ "pos": "243836",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1071000,
+ "pts_time": "11.900000",
+ "dts": 1077000,
+ "dts_time": "11.966667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "558",
+ "pos": "246092",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1077000,
+ "pts_time": "11.966667",
+ "dts": 1080000,
+ "dts_time": "12.000000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "538",
+ "pos": "247596",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1092000,
+ "pts_time": "12.133333",
+ "dts": 1083000,
+ "dts_time": "12.033333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9278",
+ "pos": "248724",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1086000,
+ "pts_time": "12.066667",
+ "dts": 1086000,
+ "dts_time": "12.066667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1625",
+ "pos": "259064",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1083000,
+ "pts_time": "12.033333",
+ "dts": 1089000,
+ "dts_time": "12.100000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "518",
+ "pos": "260944",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1089000,
+ "pts_time": "12.100000",
+ "dts": 1092000,
+ "dts_time": "12.133333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "557",
+ "pos": "261884",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1104000,
+ "pts_time": "12.266667",
+ "dts": 1095000,
+ "dts_time": "12.166667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8101",
+ "pos": "263388",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1098000,
+ "pts_time": "12.200000",
+ "dts": 1098000,
+ "dts_time": "12.200000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1464",
+ "pos": "272224",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1095000,
+ "pts_time": "12.166667",
+ "dts": 1101000,
+ "dts_time": "12.233333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "426",
+ "pos": "274668",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1101000,
+ "pts_time": "12.233333",
+ "dts": 1104000,
+ "dts_time": "12.266667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "417",
+ "pos": "275608",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1116000,
+ "pts_time": "12.400000",
+ "dts": 1107000,
+ "dts_time": "12.300000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7971",
+ "pos": "276924",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1110000,
+ "pts_time": "12.333333",
+ "dts": 1110000,
+ "dts_time": "12.333333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1443",
+ "pos": "285384",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1107000,
+ "pts_time": "12.300000",
+ "dts": 1113000,
+ "dts_time": "12.366667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "476",
+ "pos": "287264",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1113000,
+ "pts_time": "12.366667",
+ "dts": 1116000,
+ "dts_time": "12.400000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "423",
+ "pos": "288580",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1128000,
+ "pts_time": "12.533333",
+ "dts": 1119000,
+ "dts_time": "12.433333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7882",
+ "pos": "289520",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1122000,
+ "pts_time": "12.466667",
+ "dts": 1122000,
+ "dts_time": "12.466667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1236",
+ "pos": "298356",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1119000,
+ "pts_time": "12.433333",
+ "dts": 1125000,
+ "dts_time": "12.500000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "445",
+ "pos": "300048",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1125000,
+ "pts_time": "12.500000",
+ "dts": 1128000,
+ "dts_time": "12.533333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "428",
+ "pos": "301176",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1140000,
+ "pts_time": "12.666667",
+ "dts": 1131000,
+ "dts_time": "12.566667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8139",
+ "pos": "302116",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1134000,
+ "pts_time": "12.600000",
+ "dts": 1134000,
+ "dts_time": "12.600000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1227",
+ "pos": "310952",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1131000,
+ "pts_time": "12.566667",
+ "dts": 1137000,
+ "dts_time": "12.633333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "440",
+ "pos": "313020",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1137000,
+ "pts_time": "12.633333",
+ "dts": 1140000,
+ "dts_time": "12.666667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "467",
+ "pos": "313960",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1152000,
+ "pts_time": "12.800000",
+ "dts": 1143000,
+ "dts_time": "12.700000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8607",
+ "pos": "315276",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1146000,
+ "pts_time": "12.733333",
+ "dts": 1146000,
+ "dts_time": "12.733333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1283",
+ "pos": "324300",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1143000,
+ "pts_time": "12.700000",
+ "dts": 1149000,
+ "dts_time": "12.766667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "462",
+ "pos": "326556",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1149000,
+ "pts_time": "12.766667",
+ "dts": 1152000,
+ "dts_time": "12.800000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "450",
+ "pos": "327496",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1164000,
+ "pts_time": "12.933333",
+ "dts": 1155000,
+ "dts_time": "12.833333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8385",
+ "pos": "328812",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1158000,
+ "pts_time": "12.866667",
+ "dts": 1158000,
+ "dts_time": "12.866667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1292",
+ "pos": "337836",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1155000,
+ "pts_time": "12.833333",
+ "dts": 1161000,
+ "dts_time": "12.900000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "429",
+ "pos": "339716",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1161000,
+ "pts_time": "12.900000",
+ "dts": 1164000,
+ "dts_time": "12.933333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "381",
+ "pos": "341032",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1167000,
+ "pts_time": "12.966667",
+ "dts": 1167000,
+ "dts_time": "12.966667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "3383",
+ "pos": "341784",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1170000,
+ "pts_time": "13.000000",
+ "dts": 1170000,
+ "dts_time": "13.000000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "4102",
+ "pos": "346108",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1182000,
+ "pts_time": "13.133333",
+ "dts": 1173000,
+ "dts_time": "13.033333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7081",
+ "pos": "350808",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1176000,
+ "pts_time": "13.066667",
+ "dts": 1176000,
+ "dts_time": "13.066667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1068",
+ "pos": "358892",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1173000,
+ "pts_time": "13.033333",
+ "dts": 1179000,
+ "dts_time": "13.100000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "377",
+ "pos": "360396",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1179000,
+ "pts_time": "13.100000",
+ "dts": 1182000,
+ "dts_time": "13.133333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "340",
+ "pos": "361336",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1194000,
+ "pts_time": "13.266667",
+ "dts": 1185000,
+ "dts_time": "13.166667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8008",
+ "pos": "362276",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1188000,
+ "pts_time": "13.200000",
+ "dts": 1188000,
+ "dts_time": "13.200000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1307",
+ "pos": "370924",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1185000,
+ "pts_time": "13.166667",
+ "dts": 1191000,
+ "dts_time": "13.233333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "401",
+ "pos": "373180",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1191000,
+ "pts_time": "13.233333",
+ "dts": 1194000,
+ "dts_time": "13.266667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "473",
+ "pos": "374120",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1206000,
+ "pts_time": "13.400000",
+ "dts": 1197000,
+ "dts_time": "13.300000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9976",
+ "pos": "375436",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1200000,
+ "pts_time": "13.333333",
+ "dts": 1200000,
+ "dts_time": "13.333333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1684",
+ "pos": "386152",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1197000,
+ "pts_time": "13.300000",
+ "dts": 1203000,
+ "dts_time": "13.366667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "580",
+ "pos": "388220",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1203000,
+ "pts_time": "13.366667",
+ "dts": 1206000,
+ "dts_time": "13.400000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "606",
+ "pos": "389724",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1218000,
+ "pts_time": "13.533333",
+ "dts": 1209000,
+ "dts_time": "13.433333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "9788",
+ "pos": "390852",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1212000,
+ "pts_time": "13.466667",
+ "dts": 1212000,
+ "dts_time": "13.466667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1811",
+ "pos": "401756",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1209000,
+ "pts_time": "13.433333",
+ "dts": 1215000,
+ "dts_time": "13.500000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "623",
+ "pos": "404012",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1215000,
+ "pts_time": "13.500000",
+ "dts": 1218000,
+ "dts_time": "13.533333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "564",
+ "pos": "405516",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1230000,
+ "pts_time": "13.666667",
+ "dts": 1221000,
+ "dts_time": "13.566667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "8952",
+ "pos": "406644",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1224000,
+ "pts_time": "13.600000",
+ "dts": 1224000,
+ "dts_time": "13.600000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1471",
+ "pos": "416420",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1221000,
+ "pts_time": "13.566667",
+ "dts": 1227000,
+ "dts_time": "13.633333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "530",
+ "pos": "418488",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1227000,
+ "pts_time": "13.633333",
+ "dts": 1230000,
+ "dts_time": "13.666667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "527",
+ "pos": "419616",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1242000,
+ "pts_time": "13.800000",
+ "dts": 1233000,
+ "dts_time": "13.700000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "6746",
+ "pos": "421120",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1236000,
+ "pts_time": "13.733333",
+ "dts": 1236000,
+ "dts_time": "13.733333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1279",
+ "pos": "428452",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1233000,
+ "pts_time": "13.700000",
+ "dts": 1239000,
+ "dts_time": "13.766667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "517",
+ "pos": "430708",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1239000,
+ "pts_time": "13.766667",
+ "dts": 1242000,
+ "dts_time": "13.800000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "462",
+ "pos": "431460",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1254000,
+ "pts_time": "13.933333",
+ "dts": 1245000,
+ "dts_time": "13.833333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "7036",
+ "pos": "432776",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1248000,
+ "pts_time": "13.866667",
+ "dts": 1248000,
+ "dts_time": "13.866667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1293",
+ "pos": "440484",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1245000,
+ "pts_time": "13.833333",
+ "dts": 1251000,
+ "dts_time": "13.900000",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "476",
+ "pos": "442364",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1251000,
+ "pts_time": "13.900000",
+ "dts": 1254000,
+ "dts_time": "13.933333",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "471",
+ "pos": "443680",
+ "flags": "___",
+ "side_data_list": [
+ {
+ "side_data_type": "MPEGTS Stream ID",
+ "id": 224
+ }
+ ]
+ },
+ {
+ "codec_type": "video",
+ "stream_index": 1,
+ "pts": 1257000,
+ "pts_time": "13.966667",
+ "dts": 1257000,
+ "dts_time": "13.966667",
+ "duration": 3000,
+ "duration_time": "0.033333",
+ "size": "1472",
+ "pos": "444620",
+ "flags": "___"
+ }
+ ]
+}
diff --git a/testfiles_temp/entry2-variant1-seg1.ts b/testfiles_temp/entry2-variant1-seg1.ts
new file mode 100644
index 0000000..6fd4391
Binary files /dev/null and b/testfiles_temp/entry2-variant1-seg1.ts differ
diff --git a/testfiles_temp/entry2-variant1.m3u8 b/testfiles_temp/entry2-variant1.m3u8
new file mode 100644
index 0000000..55386d8
--- /dev/null
+++ b/testfiles_temp/entry2-variant1.m3u8
@@ -0,0 +1,45 @@
+#EXTM3U
+#EXT-X-VERSION:4
+## Created with Unified Streaming Platform (version=1.15.1-31309)
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-MEDIA-SEQUENCE:1
+#EXT-X-INDEPENDENT-SEGMENTS
+#EXT-X-TARGETDURATION:4
+#USP-X-TIMESTAMP-MAP:MPEGTS=900000,LOCAL=1970-01-01T00:00:00Z
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-1.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-2.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-3.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-4.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-5.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-6.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-7.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-8.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-9.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-10.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-11.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-12.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-13.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-14.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-15.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-16.ts
+#EXTINF:4, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-17.ts
+#EXTINF:2.8333, no desc
+manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568-18.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry2.m3u8 b/testfiles_temp/entry2.m3u8
new file mode 100644
index 0000000..4ddadd8
--- /dev/null
+++ b/testfiles_temp/entry2.m3u8
@@ -0,0 +1,15 @@
+#EXTM3U
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=960000,RESOLUTION=640x360,CODECS="mp4a.40.2,avc1.4d401e",FRAME-RATE=30.0,CLOSED-CAPTIONS=NONE
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=841568.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5220000,RESOLUTION=1920x1080,CODECS="mp4a.40.2,avc1.640028",FRAME-RATE=30.0,CLOSED-CAPTIONS=NONE
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=5099752.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2580000,RESOLUTION=1280x720,CODECS="mp4a.40.2,avc1.4d401f",FRAME-RATE=30.0,CLOSED-CAPTIONS=NONE
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=2462768.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1650000,RESOLUTION=960x540,CODECS="mp4a.40.2,avc1.4d401f",FRAME-RATE=30.0,CLOSED-CAPTIONS=NONE
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=1537008.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=760000,RESOLUTION=480x270,CODECS="mp4a.40.2,avc1.42c015",FRAME-RATE=30.0,CLOSED-CAPTIONS=NONE
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=639296.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=530000,RESOLUTION=320x180,CODECS="mp4a.40.2,avc1.42c00d",FRAME-RATE=30.0,CLOSED-CAPTIONS=NONE
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000-video_0_q7BAv3mA_Uql258jy=413600.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=120000,CODECS="mp4a.40.2"
+https://videos-cloudfront-usp.jwpsrv.com/697447fb_17168bf482c87a9ca77629506aef91ae741b18d2/sites/MDzvJl71/media/pZxWPRg4/versions/pZxWPRg4/manifest.ism/manifest-audio_0_q7BAv3mA_WbaG9LKN=112000.m3u8
diff --git a/testfiles_temp/entry3.m3u8 b/testfiles_temp/entry3.m3u8
new file mode 100644
index 0000000..27cb7ec
--- /dev/null
+++ b/testfiles_temp/entry3.m3u8
@@ -0,0 +1,133 @@
+#EXTM3U
+#EXT-X-VERSION:3
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-TARGETDURATION:11
+#EXTINF:10.000,
+url_846/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_847/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_848/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_849/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_850/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_851/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_852/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:9.967,
+url_853/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.033,
+url_854/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_855/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_856/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_857/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_858/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_859/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_860/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_861/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_862/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_863/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_864/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_865/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_866/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_867/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_868/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_869/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_870/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_871/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_872/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_873/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_874/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:9.967,
+url_875/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.033,
+url_876/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_877/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_878/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_879/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_880/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_881/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_882/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_883/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_884/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_885/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_886/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_887/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_888/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:9.967,
+url_889/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.033,
+url_890/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_891/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_892/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_893/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_894/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_895/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_896/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_897/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_898/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_899/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_900/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_901/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_902/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_903/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_904/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_905/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_906/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_907/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:10.000,
+url_908/193039199_mp4_h264_aac_hq_7.ts
+#EXTINF:4.600,
+url_909/193039199_mp4_h264_aac_hq_7.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry4.m3u8 b/testfiles_temp/entry4.m3u8
new file mode 100644
index 0000000..5e13561
--- /dev/null
+++ b/testfiles_temp/entry4.m3u8
@@ -0,0 +1,13 @@
+#EXTM3U
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=187738,RESOLUTION=416x234
+stream_110k_48k_416x234.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=274228,RESOLUTION=416x234
+stream_200k_48k_416x234.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=466428,RESOLUTION=416x234
+stream_400k_48k_416x234.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=658628,RESOLUTION=640x360
+stream_600k_48k_640x360.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850828,RESOLUTION=640x360
+stream_800k_48k_640x360.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1043028,RESOLUTION=640x360
+stream_1000k_48k_640x360.m3u8
diff --git a/testfiles_temp/entry5-2.m3u8 b/testfiles_temp/entry5-2.m3u8
new file mode 100644
index 0000000..79e1a52
--- /dev/null
+++ b/testfiles_temp/entry5-2.m3u8
@@ -0,0 +1,78 @@
+#EXTM3U
+#EXT-X-YOSPACE-ANALYTICS-URL:"https://csm-e-cebteurxaws416-2hjvnjz85og7.tls1.yospace.com/csm/analytics;jsessionid=E97CB20FD3BCE93F89ADDB12E8C82895.csm-e-cebteurxaws416-2hjvnjz85og7.tls1.yospace.com"
+#EXT-X-TARGETDURATION:10
+#EXT-X-VERSION:3
+#EXT-X-PLAYLIST-TYPE:EVENT
+#EXT-X-KEY:METHOD=AES-128,URI="key1.json?f=1041&s=0&p=1822767&m=1506045858",IV=0x000000000000000000000000001BD02F
+#EXTINF:10,
+1041_6_1822767.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key2.json?f=1041&s=0&p=1822768&m=1506045858",IV=0x000000000000000000000000001BD030
+#EXTINF:10,
+1041_6_1822768.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key3.json?f=1041&s=0&p=1822769&m=1506045858",IV=0x000000000000000000000000001BD031
+#EXTINF:10,
+1041_6_1822769.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key4.json?f=1041&s=0&p=1822770&m=1506045858",IV=0x000000000000000000000000001BD032
+#EXTINF:6.28,
+1041_6_1822770.ts?m=1506045858
+#EXT-X-DISCONTINUITY
+#EXT-X-KEY:METHOD=NONE
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-2.ts
+#EXTINF:7.2,
+u-6400-m-720x408-1628-a-96-1-3.ts
+#EXTINF:2.8,
+u-6400-m-720x408-1628-a-96-1-4.ts
+#EXT-X-DISCONTINUITY
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-1-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-2-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-3-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-4-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-5.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-6.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-7.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-8.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-9.ts
+#EXTINF:7.56,
+u-6400-m-720x408-1628-a-96-1-10.ts
+#EXTINF:2.44,
+u-6400-m-720x408-1628-a-96-1-11.ts
+#EXT-X-DISCONTINUITY
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-1-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-3-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-4-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-5-1.ts
+#EXT-X-DISCONTINUITY
+#EXT-X-KEY:METHOD=AES-128,URI="key5.json?f=1041&s=0&p=1822791&m=1506045858",IV=0x000000000000000000000000001BD047
+#EXTINF:9.72,
+1041_6_1822791.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key6.json?f=1041&s=0&p=1822792&m=1506045858",IV=0x000000000000000000000000001BD048
+#EXTINF:10,
+1041_6_1822792.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key7.json?f=1041&s=0&p=1822793&m=1506045858",IV=0x000000000000000000000000001BD049
+#EXTINF:10,
+1041_6_1822793.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key8.json?f=1041&s=0&p=1822794&m=1506045858",IV=0x000000000000000000000000001BD04A
+#EXTINF:10,
+1041_6_1822794.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key9.json?f=1041&s=0&p=1822795&m=1506045858",IV=0x000000000000000000000000001BD04B
+#EXTINF:10,
+1041_6_1822795.ts?m=1506045858
+#EXT-X-ENDLIST
\ No newline at end of file
diff --git a/testfiles_temp/entry5.m3u8 b/testfiles_temp/entry5.m3u8
new file mode 100644
index 0000000..79e1a52
--- /dev/null
+++ b/testfiles_temp/entry5.m3u8
@@ -0,0 +1,78 @@
+#EXTM3U
+#EXT-X-YOSPACE-ANALYTICS-URL:"https://csm-e-cebteurxaws416-2hjvnjz85og7.tls1.yospace.com/csm/analytics;jsessionid=E97CB20FD3BCE93F89ADDB12E8C82895.csm-e-cebteurxaws416-2hjvnjz85og7.tls1.yospace.com"
+#EXT-X-TARGETDURATION:10
+#EXT-X-VERSION:3
+#EXT-X-PLAYLIST-TYPE:EVENT
+#EXT-X-KEY:METHOD=AES-128,URI="key1.json?f=1041&s=0&p=1822767&m=1506045858",IV=0x000000000000000000000000001BD02F
+#EXTINF:10,
+1041_6_1822767.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key2.json?f=1041&s=0&p=1822768&m=1506045858",IV=0x000000000000000000000000001BD030
+#EXTINF:10,
+1041_6_1822768.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key3.json?f=1041&s=0&p=1822769&m=1506045858",IV=0x000000000000000000000000001BD031
+#EXTINF:10,
+1041_6_1822769.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key4.json?f=1041&s=0&p=1822770&m=1506045858",IV=0x000000000000000000000000001BD032
+#EXTINF:6.28,
+1041_6_1822770.ts?m=1506045858
+#EXT-X-DISCONTINUITY
+#EXT-X-KEY:METHOD=NONE
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-2.ts
+#EXTINF:7.2,
+u-6400-m-720x408-1628-a-96-1-3.ts
+#EXTINF:2.8,
+u-6400-m-720x408-1628-a-96-1-4.ts
+#EXT-X-DISCONTINUITY
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-1-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-2-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-3-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-4-1.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-5.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-6.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-7.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-8.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-9.ts
+#EXTINF:7.56,
+u-6400-m-720x408-1628-a-96-1-10.ts
+#EXTINF:2.44,
+u-6400-m-720x408-1628-a-96-1-11.ts
+#EXT-X-DISCONTINUITY
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-1-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-3-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-4-2.ts
+#EXTINF:10,
+u-6400-m-720x408-1628-a-96-1-5-1.ts
+#EXT-X-DISCONTINUITY
+#EXT-X-KEY:METHOD=AES-128,URI="key5.json?f=1041&s=0&p=1822791&m=1506045858",IV=0x000000000000000000000000001BD047
+#EXTINF:9.72,
+1041_6_1822791.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key6.json?f=1041&s=0&p=1822792&m=1506045858",IV=0x000000000000000000000000001BD048
+#EXTINF:10,
+1041_6_1822792.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key7.json?f=1041&s=0&p=1822793&m=1506045858",IV=0x000000000000000000000000001BD049
+#EXTINF:10,
+1041_6_1822793.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key8.json?f=1041&s=0&p=1822794&m=1506045858",IV=0x000000000000000000000000001BD04A
+#EXTINF:10,
+1041_6_1822794.ts?m=1506045858
+#EXT-X-KEY:METHOD=AES-128,URI="key9.json?f=1041&s=0&p=1822795&m=1506045858",IV=0x000000000000000000000000001BD04B
+#EXTINF:10,
+1041_6_1822795.ts?m=1506045858
+#EXT-X-ENDLIST
\ No newline at end of file
diff --git a/testfiles_temp/entry6.m3u8 b/testfiles_temp/entry6.m3u8
new file mode 100644
index 0000000..b802f56
--- /dev/null
+++ b/testfiles_temp/entry6.m3u8
@@ -0,0 +1,78 @@
+#EXTM3U
+#EXT-X-VERSION:4
+#EXT-X-ALLOW-CACHE:YES
+#EXT-X-TARGETDURATION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:501584@0
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:3.560000,
+#EXT-X-BYTERANGE:1005424@501584
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.360000,
+#EXT-X-BYTERANGE:567196@1507008
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:423752@2074204
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:434844@2497956
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:0.480000,
+#EXT-X-BYTERANGE:128404@2932800
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:435408@3061204
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:488988@3496612
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:1.960000,
+#EXT-X-BYTERANGE:482784@3985600
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:350620@4468384
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:328812@4819004
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.920000,
+#EXT-X-BYTERANGE:677364@5147816
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:466804@5825180
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:532980@6291984
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:524144@6824964
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:517564@7349108
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.520000,
+#EXT-X-BYTERANGE:647472@7866672
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:579228@8514144
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:0.600000,
+#EXT-X-BYTERANGE:177284@9093372
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.760000,
+#EXT-X-BYTERANGE:560052@9270656
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:395176@9830708
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:479024@10225884
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:2.000000,
+#EXT-X-BYTERANGE:481468@10704908
+cisq0gim60007xzvi505emlxx.ts
+#EXTINF:0.080000,
+#EXT-X-BYTERANGE:93812@11186376
+cisq0gim60007xzvi505emlxx.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry7.m3u8 b/testfiles_temp/entry7.m3u8
new file mode 100644
index 0000000..84b79fb
--- /dev/null
+++ b/testfiles_temp/entry7.m3u8
@@ -0,0 +1,43 @@
+#EXTM3U
+#EXT-X-MEDIA-SEQUENCE:1
+#EXT-X-TARGETDURATION:10
+#EXT-X-ALLOW-CACHE:YES
+#EXTINF:10,
+130130211307_1.ts
+#EXTINF:10,
+130130211307_2.ts
+#EXTINF:10,
+130130211307_3.ts
+#EXTINF:10,
+130130211307_4.ts
+#EXTINF:10,
+130130211307_5.ts
+#EXTINF:10,
+130130211307_6.ts
+#EXTINF:10,
+130130211307_7.ts
+#EXTINF:10,
+130130211307_8.ts
+#EXTINF:10,
+130130211307_9.ts
+#EXTINF:10,
+130130211307_10.ts
+#EXTINF:10,
+130130211307_11.ts
+#EXTINF:10,
+130130211307_12.ts
+#EXTINF:10,
+130130211307_13.ts
+#EXTINF:10,
+130130211307_14.ts
+#EXTINF:10,
+130130211307_15.ts
+#EXTINF:10,
+130130211307_16.ts
+#EXTINF:10,
+130130211307_17.ts
+#EXTINF:10,
+130130211307_18.ts
+#EXTINF:10,
+130130211307_19.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry8-variant1.m3u8 b/testfiles_temp/entry8-variant1.m3u8
new file mode 100644
index 0000000..1b544b6
--- /dev/null
+++ b/testfiles_temp/entry8-variant1.m3u8
@@ -0,0 +1,38 @@
+#EXTM3U
+#EXT-X-VERSION:1
+## Created with Unified Streaming Platform(version=1.6.7)
+#EXT-X-MEDIA-SEQUENCE:1
+#EXT-X-ALLOW-CACHE:NO
+#EXT-X-TARGETDURATION:11
+#EXT-X-KEY:METHOD=AES-128,URI="oceans.key"
+#EXTINF:11, no desc
+oceans_aes-audio=65000-video=236000-1.ts
+#EXTINF:7, no desc
+oceans_aes-audio=65000-video=236000-2.ts
+#EXTINF:7, no desc
+oceans_aes-audio=65000-video=236000-3.ts
+#EXTINF:8, no desc
+oceans_aes-audio=65000-video=236000-4.ts
+#EXTINF:10, no desc
+oceans_aes-audio=65000-video=236000-5.ts
+#EXTINF:6, no desc
+oceans_aes-audio=65000-video=236000-6.ts
+#EXTINF:9, no desc
+oceans_aes-audio=65000-video=236000-7.ts
+#EXTINF:7, no desc
+oceans_aes-audio=65000-video=236000-8.ts
+#EXTINF:9, no desc
+oceans_aes-audio=65000-video=236000-9.ts
+#EXTINF:8, no desc
+oceans_aes-audio=65000-video=236000-10.ts
+#EXTINF:8, no desc
+oceans_aes-audio=65000-video=236000-11.ts
+#EXTINF:8, no desc
+oceans_aes-audio=65000-video=236000-12.ts
+#EXTINF:6, no desc
+oceans_aes-audio=65000-video=236000-13.ts
+#EXTINF:9, no desc
+oceans_aes-audio=65000-video=236000-14.ts
+#EXTINF:5, no desc
+oceans_aes-audio=65000-video=236000-15.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry8.m3u8 b/testfiles_temp/entry8.m3u8
new file mode 100644
index 0000000..e7f9da3
--- /dev/null
+++ b/testfiles_temp/entry8.m3u8
@@ -0,0 +1,17 @@
+#EXTM3U
+#EXT-X-VERSION:1
+## Created with Unified Streaming Platform(version=1.6.7)
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=319060,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=304x128
+oceans_aes-audio=65000-video=236000.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=461100,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=384x160
+oceans_aes-audio=65000-video=370000.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=674160,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=544x224
+oceans_aes-audio=65000-video=571000.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=999580,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=736x304
+oceans_aes-audio=65000-video=878000.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1489300,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=960x400
+oceans_aes-audio=65000-video=1340000.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2233420,CODECS="mp4a.40.2,avc1.66.31",RESOLUTION=1264x528
+oceans_aes-audio=65000-video=2042000.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=68900,CODECS="mp4a.40.2"
+oceans_aes-audio=65000.m3u8
diff --git a/testfiles_temp/entry9-variant1.m3u8 b/testfiles_temp/entry9-variant1.m3u8
new file mode 100644
index 0000000..6790a06
--- /dev/null
+++ b/testfiles_temp/entry9-variant1.m3u8
@@ -0,0 +1,17 @@
+#EXTM3U
+#EXT-X-VERSION:4
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-TARGETDURATION:6
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-KEY:METHOD=AES-128,URI="key.bin"
+#EXTINF:6.016000,
+segment-0.aac
+#EXTINF:6.016000,
+segment-1.aac
+#EXTINF:6.016000,
+segment-2.aac
+#EXTINF:6.016000,
+segment-3.aac
+#EXTINF:5.482667,
+segment-4.aac
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry9-variant3.m3u8 b/testfiles_temp/entry9-variant3.m3u8
new file mode 100644
index 0000000..a9f2a86
--- /dev/null
+++ b/testfiles_temp/entry9-variant3.m3u8
@@ -0,0 +1,33 @@
+#EXTM3U
+#EXT-X-VERSION:4
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-I-FRAMES-ONLY
+#EXT-X-INDEPENDENT-SEGMENTS
+#EXT-X-TARGETDURATION:8
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-KEY:METHOD=AES-128,URI="key.bin"
+#EXTINF:8.400000,
+#EXT-X-BYTERANGE:107724@376
+segment-0.ts
+#EXTINF:4.560000,
+#EXT-X-BYTERANGE:38540@376
+segment-1.ts
+#EXTINF:3.120000,
+#EXT-X-BYTERANGE:46060@176720
+segment-1.ts
+#EXTINF:5.640000,
+#EXT-X-BYTERANGE:125208@376
+segment-2.ts
+#EXTINF:1.880000,
+#EXT-X-BYTERANGE:89676@902964
+segment-2.ts
+#EXTINF:2.160000,
+#EXT-X-BYTERANGE:125396@376
+segment-3.ts
+#EXTINF:2.080000,
+#EXT-X-BYTERANGE:52076@232744
+segment-3.ts
+#EXTINF:1.680000,
+#EXT-X-BYTERANGE:117688@627920
+segment-3.ts
+#EXT-X-ENDLIST
diff --git a/testfiles_temp/entry9.m3u8 b/testfiles_temp/entry9.m3u8
new file mode 100644
index 0000000..3333a05
--- /dev/null
+++ b/testfiles_temp/entry9.m3u8
@@ -0,0 +1,14 @@
+#EXTM3U
+# Created with Bento4 mp4-hls.py version 1.2.0r637
+
+#EXT-X-VERSION:4
+
+# Audio
+#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_aac",NAME="Unknown",LANGUAGE="und",AUTOSELECT=YES,DEFAULT=YES,URI="audio/aac/und/stream.m3u8"
+
+# Media Playlists
+#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1468268,BANDWIDTH=2051101,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=1280x720,AUDIO="audio_aac"
+media-1/stream.m3u8
+
+# I-Frame Playlists
+#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=190343,BANDWIDTH=560419,CODECS="avc1.4D401F",RESOLUTION=1280x720,URI="media-1/iframes.m3u8"
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 0000000..c80b36c
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,12 @@
+- "manifest" -> "playlist"?
+- Retaining Input instances for segments without clogging up memory indefinitely
+
+idea: discontinuities with extra decoder config on the packet.
+Also, why not just have the packet metadata on the packet? I think that would make things easier overall.
+Also: for robustness, do a different track matching algorithm for hls playback. so not use pid but make it simpler like
+ if theres only 1 video track then its obvious yknow. Need to see if this is actually needed in the case of ext-x-discontinuity
+
+- EXT-X-MEDIA
+- getSegments() API? Any point in partially reading the playlist file? Idk
+
+dont forget ext x media + stream inf both situation
\ No newline at end of file