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