init commit

This commit is contained in:
Carlos
2026-02-21 10:33:18 +01:00
parent c863a943ed
commit 9d955bf338
9512 changed files with 2015317 additions and 1305 deletions

View File

@@ -0,0 +1,18 @@
import type { MediaParserLogLevel } from '../../log';
import type { MediaParserReaderInterface } from '../../readers/reader';
import type { CanSkipTracksState } from '../../state/can-skip-tracks';
import type { M3uState } from '../../state/m3u-state';
import type { MediaParserOnAudioTrack } from '../../webcodec-sample-types';
import type { SelectM3uAssociatedPlaylistsFn, SelectM3uStreamFn } from './select-stream';
import type { M3uStructure } from './types';
export declare const afterManifestFetch: ({ structure, m3uState, src, selectM3uStreamFn, logLevel, selectAssociatedPlaylistsFn, readerInterface, onAudioTrack, canSkipTracks, }: {
structure: M3uStructure;
m3uState: M3uState;
src: string;
selectM3uStreamFn: SelectM3uStreamFn;
selectAssociatedPlaylistsFn: SelectM3uAssociatedPlaylistsFn;
logLevel: MediaParserLogLevel;
readerInterface: MediaParserReaderInterface;
onAudioTrack: MediaParserOnAudioTrack | null;
canSkipTracks: CanSkipTracksState;
}) => Promise<void>;

View File

@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.afterManifestFetch = void 0;
const log_1 = require("../../log");
const fetch_m3u8_stream_1 = require("./fetch-m3u8-stream");
const get_streams_1 = require("./get-streams");
const select_stream_1 = require("./select-stream");
const afterManifestFetch = async ({ structure, m3uState, src, selectM3uStreamFn, logLevel, selectAssociatedPlaylistsFn, readerInterface, onAudioTrack, canSkipTracks, }) => {
const independentSegments = (0, get_streams_1.isIndependentSegments)(structure);
const streams = (0, get_streams_1.getM3uStreams)({ structure, originalSrc: src, readerInterface });
// Handle single media playlists (not master playlists):
// 1. If !independentSegments: Old-style single playlist without segment independence
// 2. If streams === null: Single media playlist (has EXT-X-INDEPENDENT-SEGMENTS but no EXT-X-STREAM-INF)
// Both cases should iterate over the current URL as the media playlist
if (!independentSegments || streams === null) {
if (!src) {
throw new Error('No src');
}
m3uState.setSelectedMainPlaylist({
type: 'initial-url',
url: src,
});
return m3uState.setReadyToIterateOverM3u();
}
const selectedPlaylist = await (0, select_stream_1.selectStream)({ streams, fn: selectM3uStreamFn });
if (!selectedPlaylist.dimensions) {
throw new Error('Stream does not have a resolution');
}
m3uState.setSelectedMainPlaylist({
type: 'selected-stream',
stream: selectedPlaylist,
});
const skipAudioTracks = onAudioTrack === null && canSkipTracks.doFieldsNeedTracks() === false;
const associatedPlaylists = await (0, select_stream_1.selectAssociatedPlaylists)({
playlists: selectedPlaylist.associatedPlaylists,
fn: selectAssociatedPlaylistsFn,
skipAudioTracks,
});
m3uState.setAssociatedPlaylists(associatedPlaylists);
const playlistUrls = [
selectedPlaylist.src,
...associatedPlaylists.map((p) => p.src),
];
const struc = await Promise.all(playlistUrls.map(async (url) => {
log_1.Log.verbose(logLevel, `Fetching playlist ${url}`);
const boxes = await (0, fetch_m3u8_stream_1.fetchM3u8Stream)({ url, readerInterface });
return {
type: 'm3u-playlist',
boxes,
src: url,
};
}));
structure.boxes.push(...struc);
m3uState.setReadyToIterateOverM3u();
};
exports.afterManifestFetch = afterManifestFetch;

View File

@@ -0,0 +1,6 @@
import type { MediaParserReaderInterface } from '../../readers/reader';
import type { M3uBox } from './types';
export declare const fetchM3u8Stream: ({ url, readerInterface, }: {
url: string;
readerInterface: MediaParserReaderInterface;
}) => Promise<M3uBox[]>;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchM3u8Stream = void 0;
const parse_m3u8_text_1 = require("./parse-m3u8-text");
const fetchM3u8Stream = async ({ url, readerInterface, }) => {
const text = await readerInterface.readWholeAsText(url);
const lines = text.split('\n');
const boxes = [];
for (const line of lines) {
(0, parse_m3u8_text_1.parseM3u8Text)(line.trim(), boxes);
}
return boxes;
};
exports.fetchM3u8Stream = fetchM3u8Stream;

View File

@@ -0,0 +1,13 @@
import type { MediaParserController } from '../../controller/media-parser-controller';
import type { M3uState } from '../../state/m3u-state';
import type { MediaParserOnVideoSample, MediaParserVideoSample } from '../../webcodec-sample-types';
export declare const considerSeekBasedOnChunk: ({ sample, parentController, childController, callback, m3uState, playlistUrl, subtractChunks, chunkIndex, }: {
sample: MediaParserVideoSample;
callback: MediaParserOnVideoSample;
parentController: MediaParserController;
childController: MediaParserController;
playlistUrl: string;
m3uState: M3uState;
subtractChunks: number;
chunkIndex: number | null;
}) => Promise<void>;

View File

@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.considerSeekBasedOnChunk = void 0;
const webcodecs_timescale_1 = require("../../webcodecs-timescale");
const considerSeekBasedOnChunk = async ({ sample, parentController, childController, callback, m3uState, playlistUrl, subtractChunks, chunkIndex, }) => {
const pendingSeek = m3uState.getSeekToSecondsToProcess(playlistUrl);
// If there is not even a seek to consider, just call the callback
if (pendingSeek === null) {
await callback(sample);
return;
}
const timestamp = Math.min(sample.decodingTimestamp / webcodecs_timescale_1.WEBCODECS_TIMESCALE, sample.timestamp / webcodecs_timescale_1.WEBCODECS_TIMESCALE);
// Already too far, now we should go to the previous chunk
if (timestamp > pendingSeek.targetTime &&
chunkIndex !== null &&
chunkIndex > 0) {
m3uState.setNextSeekShouldSubtractChunks(playlistUrl, subtractChunks + 1);
parentController.seek(pendingSeek.targetTime);
return;
}
// We are good, we have not gone too far! Don't emit sample and seek and clear pending seek
childController.seek(pendingSeek.targetTime);
m3uState.setNextSeekShouldSubtractChunks(playlistUrl, 0);
m3uState.setSeekToSecondsToProcess(playlistUrl, null);
};
exports.considerSeekBasedOnChunk = considerSeekBasedOnChunk;

View File

@@ -0,0 +1,7 @@
import type { M3uPlaylist } from './types';
export type M3uChunk = {
duration: number;
url: string;
isHeader: boolean;
};
export declare const getChunks: (playlist: M3uPlaylist) => M3uChunk[];

View File

@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChunks = void 0;
const getChunks = (playlist) => {
const chunks = [];
for (let i = 0; i < playlist.boxes.length; i++) {
const box = playlist.boxes[i];
if (box.type === 'm3u-map') {
chunks.push({ duration: 0, url: box.value, isHeader: true });
continue;
}
if (box.type === 'm3u-extinf') {
const nextBox = playlist.boxes[i + 1];
i++;
if (nextBox.type !== 'm3u-text-value') {
throw new Error('Expected m3u-text-value');
}
chunks.push({ duration: box.value, url: nextBox.value, isHeader: false });
}
continue;
}
return chunks;
};
exports.getChunks = getChunks;

View File

@@ -0,0 +1,2 @@
import type { ParserState } from '../../state/parser-state';
export declare const getDurationFromM3u: (state: ParserState) => number | null;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDurationFromM3u = void 0;
const get_playlist_1 = require("./get-playlist");
const getDurationFromM3u = (state) => {
const playlists = (0, get_playlist_1.getAllPlaylists)({
structure: state.structure.getM3uStructure(),
src: state.src,
});
return Math.max(...playlists.map((p) => {
return (0, get_playlist_1.getDurationFromPlaylist)(p);
}));
};
exports.getDurationFromM3u = getDurationFromM3u;

View File

@@ -0,0 +1,8 @@
import type { ParseMediaSrc } from '../../options';
import type { M3uPlaylist, M3uStructure } from './types';
export declare const getAllPlaylists: ({ structure, src, }: {
structure: M3uStructure;
src: ParseMediaSrc;
}) => M3uPlaylist[];
export declare const getPlaylist: (structure: M3uStructure, src: string) => M3uPlaylist;
export declare const getDurationFromPlaylist: (playlist: M3uPlaylist) => number;

View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDurationFromPlaylist = exports.getPlaylist = exports.getAllPlaylists = void 0;
const get_streams_1 = require("./get-streams");
const getAllPlaylists = ({ structure, src, }) => {
const isIndependent = (0, get_streams_1.isIndependentSegments)(structure);
if (!isIndependent) {
return [
{
type: 'm3u-playlist',
boxes: structure.boxes,
src,
},
];
}
const playlists = structure.boxes.filter((box) => box.type === 'm3u-playlist');
// If no playlists found, this might be a single media playlist (not a master playlist)
// Create a synthetic playlist from the structure boxes
if (playlists.length === 0) {
return [
{
type: 'm3u-playlist',
boxes: structure.boxes,
src,
},
];
}
return playlists;
};
exports.getAllPlaylists = getAllPlaylists;
const getPlaylist = (structure, src) => {
const allPlaylists = (0, exports.getAllPlaylists)({ structure, src });
const playlists = allPlaylists.find((box) => box.src === src);
if (!playlists) {
throw new Error(`Expected m3u-playlist with src ${src}`);
}
return playlists;
};
exports.getPlaylist = getPlaylist;
const getDurationFromPlaylist = (playlist) => {
const duration = playlist.boxes.filter((box) => box.type === 'm3u-extinf');
if (duration.length === 0) {
throw new Error('Expected duration in m3u playlist');
}
return duration.reduce((acc, d) => acc + d.value, 0);
};
exports.getDurationFromPlaylist = getDurationFromPlaylist;

View File

@@ -0,0 +1,13 @@
import type { MediaParserLogLevel } from '../../log';
import type { M3uState } from '../../state/m3u-state';
import type { SeekResolution } from '../../work-on-seek-request';
export declare const clearM3uStateInPrepareForSeek: ({ m3uState, logLevel, }: {
m3uState: M3uState;
logLevel: MediaParserLogLevel;
}) => void;
export declare const getSeekingByteForM3u8: ({ time, currentPosition, m3uState, logLevel, }: {
time: number;
currentPosition: number;
m3uState: M3uState;
logLevel: MediaParserLogLevel;
}) => SeekResolution;

View File

@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSeekingByteForM3u8 = exports.clearM3uStateInPrepareForSeek = void 0;
const log_1 = require("../../log");
const clearM3uStateInPrepareForSeek = ({ m3uState, logLevel, }) => {
const selectedPlaylists = m3uState.getSelectedPlaylists();
for (const playlistUrl of selectedPlaylists) {
const streamRun = m3uState.getM3uStreamRun(playlistUrl);
if (streamRun) {
streamRun.abort();
}
log_1.Log.trace(logLevel, 'Clearing M3U stream run for', playlistUrl);
m3uState.setM3uStreamRun(playlistUrl, null);
}
m3uState.clearAllChunksProcessed();
m3uState.sampleSorter.clearSamples();
};
exports.clearM3uStateInPrepareForSeek = clearM3uStateInPrepareForSeek;
const getSeekingByteForM3u8 = ({ time, currentPosition, m3uState, logLevel, }) => {
(0, exports.clearM3uStateInPrepareForSeek)({ m3uState, logLevel });
const selectedPlaylists = m3uState.getSelectedPlaylists();
for (const playlistUrl of selectedPlaylists) {
m3uState.setSeekToSecondsToProcess(playlistUrl, {
targetTime: time,
});
}
return {
type: 'do-seek',
byte: currentPosition,
// TODO: This will be imperfect when seeking in playMedia()
timeInSeconds: time,
};
};
exports.getSeekingByteForM3u8 = getSeekingByteForM3u8;

View File

@@ -0,0 +1,32 @@
import type { MediaParserDimensions } from '../../get-dimensions';
import type { ParseMediaSrc } from '../../options';
import type { MediaParserStructureUnstable } from '../../parse-result';
import type { MediaParserReaderInterface } from '../../readers/reader';
import type { ParserState } from '../../state/parser-state';
export type M3uAssociatedPlaylist = {
groupId: string;
language: string | null;
name: string | null;
autoselect: boolean;
default: boolean;
channels: number | null;
src: string;
id: number;
isAudio: boolean;
};
export type M3uStream = {
src: string;
bandwidthInBitsPerSec: number | null;
averageBandwidthInBitsPerSec: number | null;
dimensions: MediaParserDimensions | null;
codecs: string[] | null;
id: number;
associatedPlaylists: M3uAssociatedPlaylist[];
};
export declare const isIndependentSegments: (structure: MediaParserStructureUnstable | null) => boolean;
export declare const getM3uStreams: ({ structure, originalSrc, readerInterface, }: {
structure: MediaParserStructureUnstable | null;
originalSrc: ParseMediaSrc;
readerInterface: MediaParserReaderInterface;
}) => M3uStream[] | null;
export declare const m3uHasStreams: (state: ParserState) => boolean;

View File

@@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.m3uHasStreams = exports.getM3uStreams = exports.isIndependentSegments = void 0;
const isIndependentSegments = (structure) => {
if (structure === null || structure.type !== 'm3u') {
return false;
}
return structure.boxes.some((box) => box.type === 'm3u-independent-segments' || box.type === 'm3u-stream-info');
};
exports.isIndependentSegments = isIndependentSegments;
const getM3uStreams = ({ structure, originalSrc, readerInterface, }) => {
if (structure === null || structure.type !== 'm3u') {
return null;
}
const boxes = [];
for (let i = 0; i < structure.boxes.length; i++) {
const str = structure.boxes[i];
if (str.type === 'm3u-stream-info') {
const next = structure.boxes[i + 1];
if (next.type !== 'm3u-text-value') {
throw new Error('Expected m3u-text-value');
}
const associatedPlaylists = [];
if (str.audio) {
const match = structure.boxes.filter((box) => {
return box.type === 'm3u-media-info' && box.groupId === str.audio;
});
for (const audioTrack of match) {
associatedPlaylists.push({
autoselect: audioTrack.autoselect,
channels: audioTrack.channels,
default: audioTrack.default,
groupId: audioTrack.groupId,
language: audioTrack.language,
name: audioTrack.name,
src: readerInterface.createAdjacentFileSource(audioTrack.uri, originalSrc),
id: associatedPlaylists.length,
isAudio: true,
});
}
}
boxes.push({
src: readerInterface.createAdjacentFileSource(next.value, originalSrc),
averageBandwidthInBitsPerSec: str.averageBandwidthInBitsPerSec,
bandwidthInBitsPerSec: str.bandwidthInBitsPerSec,
codecs: str.codecs,
dimensions: str.dimensions,
associatedPlaylists,
});
}
}
// Maybe this is already a playlist
if (boxes.length === 0) {
return null;
}
const sorted = boxes.slice().sort((a, b) => {
var _a, _b, _c, _d;
const aResolution = a.dimensions
? a.dimensions.width * a.dimensions.height
: 0;
const bResolution = b.dimensions
? b.dimensions.width * b.dimensions.height
: 0;
if (aResolution === bResolution) {
const bandwidthA = (_b = (_a = a.averageBandwidthInBitsPerSec) !== null && _a !== void 0 ? _a : a.bandwidthInBitsPerSec) !== null && _b !== void 0 ? _b : 0;
const bandwidthB = (_d = (_c = b.averageBandwidthInBitsPerSec) !== null && _c !== void 0 ? _c : b.bandwidthInBitsPerSec) !== null && _d !== void 0 ? _d : 0;
return bandwidthB - bandwidthA;
}
return bResolution - aResolution;
});
return sorted.map((box, index) => ({ ...box, id: index }));
};
exports.getM3uStreams = getM3uStreams;
const m3uHasStreams = (state) => {
const structure = state.structure.getStructureOrNull();
if (!structure) {
return false;
}
if (structure.type !== 'm3u') {
return true;
}
return state.m3u.hasFinishedManifest();
};
exports.m3uHasStreams = m3uHasStreams;

View File

@@ -0,0 +1,2 @@
import type { M3uBox } from './types';
export declare const parseM3uDirective: (str: string) => M3uBox;

View File

@@ -0,0 +1,129 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseM3uDirective = void 0;
const parse_m3u_media_directive_1 = require("./parse-m3u-media-directive");
const parse_stream_inf_1 = require("./parse-stream-inf");
const parseM3uDirective = (str) => {
const firstColon = str.indexOf(':');
const directive = (firstColon === -1 ? str : str.slice(0, firstColon)).trim();
const value = firstColon === -1 ? null : str.slice(firstColon + 1);
if (directive === '#EXT-X-VERSION') {
if (!value) {
throw new Error('EXT-X-VERSION directive must have a value');
}
return {
type: 'm3u-version',
version: value,
};
}
if (directive === '#EXT-X-INDEPENDENT-SEGMENTS') {
return {
type: 'm3u-independent-segments',
};
}
if (directive === '#EXT-X-MEDIA') {
if (!value) {
throw new Error('EXT-X-MEDIA directive must have a value');
}
const parsed = (0, parse_m3u_media_directive_1.parseM3uMediaDirective)(value);
return parsed;
}
if (directive === '#EXT-X-TARGETDURATION') {
if (!value) {
throw new Error('EXT-X-TARGETDURATION directive must have a value');
}
return {
type: 'm3u-target-duration',
duration: parseFloat(value),
};
}
if (directive === '#EXTINF') {
if (!value) {
throw new Error('EXTINF has no value');
}
return {
type: 'm3u-extinf',
value: parseFloat(value),
};
}
if (directive === '#EXT-X-ENDLIST') {
return {
type: 'm3u-endlist',
};
}
if (directive === '#EXT-X-PLAYLIST-TYPE') {
if (!value) {
throw new Error('#EXT-X-PLAYLIST-TYPE. directive must have a value');
}
return {
type: 'm3u-playlist-type',
playlistType: value,
};
}
if (directive === '#EXT-X-MEDIA-SEQUENCE') {
if (!value) {
throw new Error('#EXT-X-MEDIA-SEQUENCE directive must have a value');
}
return {
type: 'm3u-media-sequence',
value: Number(value),
};
}
if (directive === '#EXT-X-DISCONTINUITY-SEQUENCE') {
if (!value) {
throw new Error('#EXT-X-DISCONTINUITY-SEQUENCE directive must have a value');
}
return {
type: 'm3u-discontinuity-sequence',
value: Number(value),
};
}
if (directive === '#EXT-X-STREAM-INF') {
if (!value) {
throw new Error('EXT-X-STREAM-INF directive must have a value');
}
const res = (0, parse_stream_inf_1.parseStreamInf)(value);
return res;
}
if (directive === '#EXT-X-I-FRAME-STREAM-INF') {
return {
type: 'm3u-i-frame-stream-info',
};
}
if (directive === '#EXT-X-ALLOW-CACHE') {
if (!value) {
throw new Error('#EXT-X-ALLOW-CACHE directive must have a value');
}
return {
type: 'm3u-allow-cache',
allowsCache: value === 'YES',
};
}
if (directive === '#EXT-X-MAP') {
if (!value) {
throw new Error('#EXT-X-MAP directive must have a value');
}
const p = (0, parse_m3u_media_directive_1.parseM3uKeyValue)(value);
if (!p.URI) {
throw new Error('EXT-X-MAP directive must have a URI');
}
return {
type: 'm3u-map',
value: p.URI,
};
}
if (directive === '#EXT-X-PROGRAM-DATE-TIME') {
if (!value) {
throw new Error('#EXT-X-PROGRAM-DATE-TIME directive must have a value');
}
// Store the raw ISO 8601 date-time string without validation.
// This directive associates media segments with absolute dates but
// doesn't affect parsing of tracks, dimensions, or other metadata.
return {
type: 'm3u-program-date-time',
dateTime: value,
};
}
throw new Error(`Unknown directive ${directive}. Value: ${value}`);
};
exports.parseM3uDirective = parseM3uDirective;

View File

@@ -0,0 +1,8 @@
import type { BufferIterator } from '../../iterator/buffer-iterator';
import type { ParseResult } from '../../parse-result';
import type { M3uStructure } from './types';
export declare const parseM3uManifest: ({ iterator, structure, contentLength, }: {
iterator: BufferIterator;
structure: M3uStructure;
contentLength: number;
}) => Promise<ParseResult>;

View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseM3uManifest = void 0;
const parse_m3u8_text_1 = require("./parse-m3u8-text");
const parseM3uManifest = ({ iterator, structure, contentLength, }) => {
const start = iterator.startCheckpoint();
const line = iterator.readUntilLineEnd();
if (iterator.counter.getOffset() > contentLength) {
throw new Error('Unexpected end of file');
}
if (line === null) {
start.returnToCheckpoint();
return Promise.resolve(null);
}
(0, parse_m3u8_text_1.parseM3u8Text)(line.trim(), structure.boxes);
return Promise.resolve(null);
};
exports.parseM3uManifest = parseM3uManifest;

View File

@@ -0,0 +1,3 @@
import type { M3uMediaInfo } from './types';
export declare const parseM3uKeyValue: (str: string) => Record<string, string>;
export declare const parseM3uMediaDirective: (str: string) => M3uMediaInfo;

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseM3uMediaDirective = exports.parseM3uKeyValue = void 0;
const parse_stream_inf_1 = require("./parse-stream-inf");
const parseM3uKeyValue = (str) => {
const quotes = (0, parse_stream_inf_1.splitRespectingQuotes)(str);
const map = {};
for (const quote of quotes) {
const firstColon = quote.indexOf('=');
const key = firstColon === -1 ? quote : quote.slice(0, firstColon);
const value = firstColon === -1 ? null : quote.slice(firstColon + 1);
if (value === null) {
throw new Error('Value is null');
}
const actualValue = (value === null || value === void 0 ? void 0 : value.startsWith('"')) && (value === null || value === void 0 ? void 0 : value.endsWith('"'))
? value.slice(1, -1)
: value;
map[key] = actualValue;
}
return map;
};
exports.parseM3uKeyValue = parseM3uKeyValue;
const parseM3uMediaDirective = (str) => {
const map = (0, exports.parseM3uKeyValue)(str);
return {
type: 'm3u-media-info',
autoselect: map.AUTOSELECT === 'YES',
channels: map.CHANNELS ? parseInt(map.CHANNELS, 10) : null,
default: map.DEFAULT === 'YES',
groupId: map['GROUP-ID'],
language: map.LANGUAGE || null,
name: map.NAME || null,
uri: map.URI,
mediaType: map.TYPE || null,
};
};
exports.parseM3uMediaDirective = parseM3uMediaDirective;

View File

@@ -0,0 +1,4 @@
import type { ParserState } from '../../state/parser-state';
export declare const parseM3u: ({ state }: {
state: ParserState;
}) => Promise<import("../../parse-result").ParseResult>;

View File

@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseM3u = void 0;
const after_manifest_fetch_1 = require("./after-manifest-fetch");
const parse_m3u_manifest_1 = require("./parse-m3u-manifest");
const run_over_m3u_1 = require("./run-over-m3u");
const parseM3u = async ({ state }) => {
const structure = state.structure.getM3uStructure();
if (state.m3u.isReadyToIterateOverM3u()) {
const selectedPlaylists = state.m3u.getSelectedPlaylists();
const whichPlaylistToRunOver = state.m3u.sampleSorter.getNextStreamToRun(selectedPlaylists);
await (0, run_over_m3u_1.runOverM3u)({
state,
structure,
playlistUrl: whichPlaylistToRunOver,
logLevel: state.logLevel,
});
return null;
}
if (state.m3u.hasFinishedManifest()) {
if (typeof state.src !== 'string' && !(state.src instanceof URL)) {
throw new Error('Expected src to be a string');
}
state.mediaSection.addMediaSection({
start: 0,
// We do a pseudo-seek when seeking m3u, which will be the same byte
// as we are currently in, which in most cases is the end of the file.
size: state.contentLength + 1,
});
await (0, after_manifest_fetch_1.afterManifestFetch)({
structure,
m3uState: state.m3u,
src: state.src.toString(),
selectM3uStreamFn: state.selectM3uStreamFn,
logLevel: state.logLevel,
selectAssociatedPlaylistsFn: state.selectM3uAssociatedPlaylistsFn,
readerInterface: state.readerInterface,
onAudioTrack: state.onAudioTrack,
canSkipTracks: state.callbacks.canSkipTracksState,
});
return null;
}
const box = await (0, parse_m3u_manifest_1.parseM3uManifest)({
iterator: state.iterator,
structure,
contentLength: state.contentLength,
});
const isDoneNow = state.iterator.counter.getOffset() === state.contentLength;
if (isDoneNow) {
state.m3u.setHasFinishedManifest();
}
return box;
};
exports.parseM3u = parseM3u;

View File

@@ -0,0 +1,2 @@
import type { M3uBox } from './types';
export declare const parseM3u8Text: (line: string, boxes: M3uBox[]) => void;

View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseM3u8Text = void 0;
const parse_directive_1 = require("./parse-directive");
const parseM3u8Text = (line, boxes) => {
if (line === '#EXTM3U') {
boxes.push({
type: 'm3u-header',
});
return;
}
if (line.startsWith('#')) {
boxes.push((0, parse_directive_1.parseM3uDirective)(line));
return;
}
if (line.trim()) {
boxes.push({
type: 'm3u-text-value',
value: line,
});
}
};
exports.parseM3u8Text = parseM3u8Text;

View File

@@ -0,0 +1,3 @@
import type { M3uStreamInfo } from './types';
export declare function splitRespectingQuotes(input: string): string[];
export declare const parseStreamInf: (str: string) => M3uStreamInfo;

View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseStreamInf = void 0;
exports.splitRespectingQuotes = splitRespectingQuotes;
function splitRespectingQuotes(input) {
const result = [];
let currentPart = '';
let insideQuote = false;
for (let i = 0; i < input.length; i++) {
const char = input[i];
// Toggle flag when encountering a quote character.
if (char === '"') {
insideQuote = !insideQuote;
currentPart += char;
}
// If we encounter a comma and we are NOT inside a quoted substring
else if (char === ',' && !insideQuote) {
result.push(currentPart);
currentPart = '';
}
else {
currentPart += char;
}
}
// Push the last token, if any.
if (currentPart) {
result.push(currentPart);
}
return result;
}
const parseStreamInf = (str) => {
const quotes = splitRespectingQuotes(str);
const map = {};
for (const quote of quotes) {
const firstColon = quote.indexOf('=');
const key = firstColon === -1 ? quote : quote.slice(0, firstColon);
const value = firstColon === -1 ? null : quote.slice(firstColon + 1);
if (value === null) {
throw new Error('Value is null');
}
const actualValue = (value === null || value === void 0 ? void 0 : value.startsWith('"')) && (value === null || value === void 0 ? void 0 : value.endsWith('"'))
? value.slice(1, -1)
: value;
map[key] = actualValue;
}
return {
type: 'm3u-stream-info',
averageBandwidthInBitsPerSec: map['AVERAGE-BANDWIDTH']
? parseInt(map['AVERAGE-BANDWIDTH'], 10)
: null,
bandwidthInBitsPerSec: map.BANDWIDTH ? parseInt(map.BANDWIDTH, 10) : null,
codecs: map.CODECS ? map.CODECS.split(',') : null,
dimensions: map.RESOLUTION
? {
width: parseInt(map.RESOLUTION.split('x')[0], 10),
height: parseInt(map.RESOLUTION.split('x')[1], 10),
}
: null,
audio: map.AUDIO || null,
};
};
exports.parseStreamInf = parseStreamInf;

View File

@@ -0,0 +1,12 @@
import type { ParserState } from '../../state/parser-state';
import type { M3uStructure } from './types';
export type PendingSeek = {
pending: number | null;
};
export declare const processM3uChunk: ({ playlistUrl, state, structure, audioDone, videoDone, }: {
playlistUrl: string;
state: ParserState;
structure: M3uStructure;
audioDone: boolean;
videoDone: boolean;
}) => Promise<void>;

View File

@@ -0,0 +1,274 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.processM3uChunk = void 0;
const media_parser_controller_1 = require("../../controller/media-parser-controller");
const forward_controller_pause_resume_abort_1 = require("../../forward-controller-pause-resume-abort");
const parse_media_1 = require("../../parse-media");
const register_track_1 = require("../../register-track");
const with_resolvers_1 = require("../../with-resolvers");
const first_sample_in_m3u_chunk_1 = require("./first-sample-in-m3u-chunk");
const get_chunks_1 = require("./get-chunks");
const get_playlist_1 = require("./get-playlist");
const get_chunk_to_seek_to_1 = require("./seek/get-chunk-to-seek-to");
const processM3uChunk = ({ playlistUrl, state, structure, audioDone, videoDone, }) => {
const { promise, reject, resolve } = (0, with_resolvers_1.withResolvers)();
const onGlobalAudioTrack = audioDone
? null
: async (track) => {
const existingTracks = state.callbacks.tracks.getTracks();
let { trackId } = track;
while (existingTracks.find((t) => t.trackId === trackId)) {
trackId++;
}
const onAudioSample = await (0, register_track_1.registerAudioTrack)({
container: 'm3u8',
track: {
...track,
trackId,
},
registerAudioSampleCallback: state.callbacks.registerAudioSampleCallback,
tracks: state.callbacks.tracks,
logLevel: state.logLevel,
onAudioTrack: state.onAudioTrack,
});
state.m3u.sampleSorter.addToStreamWithTrack(playlistUrl);
if (onAudioSample === null) {
return null;
}
state.m3u.sampleSorter.addAudioStreamToConsider(playlistUrl, onAudioSample);
return async (sample) => {
await state.m3u.sampleSorter.addAudioSample(playlistUrl, sample);
};
};
const onGlobalVideoTrack = videoDone
? null
: async (track) => {
const existingTracks = state.callbacks.tracks.getTracks();
let { trackId } = track;
while (existingTracks.find((t) => t.trackId === trackId)) {
trackId++;
}
const onVideoSample = await (0, register_track_1.registerVideoTrack)({
container: 'm3u8',
track: {
...track,
trackId,
},
logLevel: state.logLevel,
onVideoTrack: state.onVideoTrack,
registerVideoSampleCallback: state.callbacks.registerVideoSampleCallback,
tracks: state.callbacks.tracks,
});
state.m3u.sampleSorter.addToStreamWithTrack(playlistUrl);
if (onVideoSample === null) {
return null;
}
state.m3u.sampleSorter.addVideoStreamToConsider(playlistUrl, onVideoSample);
return async (sample) => {
await state.m3u.sampleSorter.addVideoSample(playlistUrl, sample);
};
};
// This function will run through the whole playlist step by step, and pause itself
// On the next run it will continue
const pausableIterator = async () => {
const playlist = (0, get_playlist_1.getPlaylist)(structure, playlistUrl);
const chunks = (0, get_chunks_1.getChunks)(playlist);
const seekToSecondsToProcess = state.m3u.getSeekToSecondsToProcess(playlistUrl);
const chunksToSubtract = state.m3u.getNextSeekShouldSubtractChunks(playlistUrl);
let chunkIndex = null;
if (seekToSecondsToProcess !== null) {
chunkIndex = Math.max(0, (0, get_chunk_to_seek_to_1.getChunkToSeekTo)({
chunks,
seekToSecondsToProcess: seekToSecondsToProcess.targetTime,
}) - chunksToSubtract);
}
const currentPromise = {
resolver: (() => undefined),
rejector: reject,
};
const requiresHeaderToBeFetched = chunks[0].isHeader;
for (const chunk of chunks) {
const mp4HeaderSegment = state.m3u.getMp4HeaderSegment(playlistUrl);
if (requiresHeaderToBeFetched && mp4HeaderSegment && chunk.isHeader) {
continue;
}
if (chunkIndex !== null &&
chunks.indexOf(chunk) < chunkIndex &&
!chunk.isHeader) {
continue;
}
currentPromise.resolver = (newRun) => {
state.m3u.setM3uStreamRun(playlistUrl, newRun);
resolve();
};
currentPromise.rejector = reject;
const childController = (0, media_parser_controller_1.mediaParserController)();
const forwarded = (0, forward_controller_pause_resume_abort_1.forwardMediaParserControllerPauseResume)({
childController,
parentController: state.controller,
});
const nextChunk = chunks[chunks.indexOf(chunk) + 1];
if (nextChunk) {
const nextChunkSource = state.readerInterface.createAdjacentFileSource(nextChunk.url, playlistUrl);
state.readerInterface.preload({
logLevel: state.logLevel,
range: null,
src: nextChunkSource,
prefetchCache: state.prefetchCache,
});
}
const makeContinuationFn = () => {
return {
continue() {
const resolver = (0, with_resolvers_1.withResolvers)();
currentPromise.resolver = resolver.resolve;
currentPromise.rejector = resolver.reject;
childController.resume();
return resolver.promise;
},
abort() {
childController.abort();
},
};
};
const isLastChunk = chunk === chunks[chunks.length - 1];
await childController._internals.checkForAbortAndPause();
const src = state.readerInterface.createAdjacentFileSource(chunk.url, playlistUrl);
try {
const data = await (0, parse_media_1.parseMedia)({
src,
acknowledgeRemotionLicense: true,
logLevel: state.logLevel,
controller: childController,
progressIntervalInMs: 0,
onParseProgress: () => {
childController.pause();
currentPromise.resolver(makeContinuationFn());
},
fields: chunk.isHeader ? { slowStructure: true } : undefined,
onTracks: () => {
if (!state.m3u.hasEmittedDoneWithTracks(playlistUrl)) {
state.m3u.setHasEmittedDoneWithTracks(playlistUrl);
const allDone = state.m3u.setTracksDone(playlistUrl);
if (allDone) {
state.callbacks.tracks.setIsDone(state.logLevel);
}
return null;
}
},
onAudioTrack: onGlobalAudioTrack === null
? null
: async ({ track }) => {
const callbackOrFalse = state.m3u.hasEmittedAudioTrack(playlistUrl);
if (callbackOrFalse === false) {
const callback = await onGlobalAudioTrack(track);
if (!callback) {
state.m3u.setHasEmittedAudioTrack(playlistUrl, null);
return null;
}
state.m3u.setHasEmittedAudioTrack(playlistUrl, callback);
return async (sample) => {
await (0, first_sample_in_m3u_chunk_1.considerSeekBasedOnChunk)({
sample,
callback,
parentController: state.controller,
childController,
m3uState: state.m3u,
playlistUrl,
subtractChunks: chunksToSubtract,
chunkIndex,
});
};
}
if (callbackOrFalse === null) {
return null;
}
return async (sample) => {
await (0, first_sample_in_m3u_chunk_1.considerSeekBasedOnChunk)({
sample,
m3uState: state.m3u,
playlistUrl,
callback: callbackOrFalse,
parentController: state.controller,
childController,
subtractChunks: chunksToSubtract,
chunkIndex,
});
};
},
onVideoTrack: onGlobalVideoTrack === null
? null
: async ({ track }) => {
const callbackOrFalse = state.m3u.hasEmittedVideoTrack(playlistUrl);
if (callbackOrFalse === false) {
const callback = await onGlobalVideoTrack({
...track,
m3uStreamFormat: chunk.isHeader || mp4HeaderSegment ? 'mp4' : 'ts',
});
if (!callback) {
state.m3u.setHasEmittedVideoTrack(playlistUrl, null);
return null;
}
state.m3u.setHasEmittedVideoTrack(playlistUrl, callback);
return async (sample) => {
await (0, first_sample_in_m3u_chunk_1.considerSeekBasedOnChunk)({
sample,
m3uState: state.m3u,
playlistUrl,
callback,
parentController: state.controller,
childController,
subtractChunks: chunksToSubtract,
chunkIndex,
});
};
}
if (callbackOrFalse === null) {
return null;
}
return async (sample) => {
await (0, first_sample_in_m3u_chunk_1.considerSeekBasedOnChunk)({
sample,
m3uState: state.m3u,
playlistUrl,
callback: callbackOrFalse,
parentController: state.controller,
childController,
subtractChunks: chunksToSubtract,
chunkIndex,
});
};
},
reader: state.readerInterface,
makeSamplesStartAtZero: false,
m3uPlaylistContext: {
mp4HeaderSegment,
isLastChunkInPlaylist: isLastChunk,
},
});
if (chunk.isHeader) {
if (data.slowStructure.type !== 'iso-base-media') {
throw new Error('Expected an mp4 file');
}
state.m3u.setMp4HeaderSegment(playlistUrl, data.slowStructure);
}
}
catch (e) {
currentPromise.rejector(e);
throw e;
}
forwarded.cleanup();
if (!isLastChunk) {
childController.pause();
currentPromise.resolver(makeContinuationFn());
}
}
currentPromise.resolver(null);
};
const run = pausableIterator();
run.catch((err) => {
reject(err);
});
return promise;
};
exports.processM3uChunk = processM3uChunk;

View File

@@ -0,0 +1,9 @@
import type { MediaParserLogLevel } from '../../log';
import type { ParserState } from '../../state/parser-state';
import type { M3uStructure } from './types';
export declare const runOverM3u: ({ state, structure, playlistUrl, logLevel, }: {
state: ParserState;
structure: M3uStructure;
playlistUrl: string;
logLevel: MediaParserLogLevel;
}) => Promise<void>;

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runOverM3u = void 0;
const log_1 = require("../../log");
const process_m3u_chunk_1 = require("./process-m3u-chunk");
const runOverM3u = async ({ state, structure, playlistUrl, logLevel, }) => {
const tracksDone = state.m3u.getTrackDone(playlistUrl);
const hasAudioStreamToConsider = state.m3u.sampleSorter.hasAudioStreamToConsider(playlistUrl);
const hasVideoStreamToConsider = state.m3u.sampleSorter.hasVideoStreamToConsider(playlistUrl);
const audioDone = !hasAudioStreamToConsider && tracksDone;
const videoDone = !hasVideoStreamToConsider && tracksDone;
const bothDone = audioDone && videoDone;
if (bothDone) {
state.m3u.setAllChunksProcessed(playlistUrl);
return;
}
const existingRun = state.m3u.getM3uStreamRun(playlistUrl);
if (existingRun) {
log_1.Log.trace(logLevel, 'Existing M3U parsing process found for', playlistUrl);
const run = await existingRun.continue();
state.m3u.setM3uStreamRun(playlistUrl, run);
if (!run) {
state.m3u.setAllChunksProcessed(playlistUrl);
}
return;
}
log_1.Log.trace(logLevel, 'Starting new M3U parsing process for', playlistUrl);
await (0, process_m3u_chunk_1.processM3uChunk)({
playlistUrl,
state,
structure,
audioDone,
videoDone,
});
};
exports.runOverM3u = runOverM3u;

View File

@@ -0,0 +1,16 @@
import type { MediaParserLogLevel } from '../../log';
import type { MediaParserAudioSample, MediaParserOnAudioSample, MediaParserOnVideoSample, MediaParserVideoSample } from '../../webcodec-sample-types';
export declare const sampleSorter: ({ logLevel, getAllChunksProcessedForPlaylist, }: {
logLevel: MediaParserLogLevel;
getAllChunksProcessedForPlaylist: (src: string) => boolean;
}) => {
clearSamples: () => void;
addToStreamWithTrack: (src: string) => void;
addVideoStreamToConsider: (src: string, callback: MediaParserOnVideoSample) => void;
addAudioStreamToConsider: (src: string, callback: MediaParserOnAudioSample) => void;
hasAudioStreamToConsider: (src: string) => boolean;
hasVideoStreamToConsider: (src: string) => boolean;
addAudioSample: (src: string, sample: MediaParserAudioSample) => Promise<void>;
addVideoSample: (src: string, sample: MediaParserVideoSample) => Promise<void>;
getNextStreamToRun: (streams: string[]) => string;
};

View File

@@ -0,0 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sampleSorter = void 0;
const log_1 = require("../../log");
const sampleSorter = ({ logLevel, getAllChunksProcessedForPlaylist, }) => {
const streamsWithTracks = [];
const audioCallbacks = {};
const videoCallbacks = {};
let latestSample = {};
return {
clearSamples: () => {
latestSample = {};
},
addToStreamWithTrack: (src) => {
streamsWithTracks.push(src);
},
addVideoStreamToConsider: (src, callback) => {
videoCallbacks[src] = callback;
},
addAudioStreamToConsider: (src, callback) => {
audioCallbacks[src] = callback;
},
hasAudioStreamToConsider: (src) => {
return Boolean(audioCallbacks[src]);
},
hasVideoStreamToConsider: (src) => {
return Boolean(videoCallbacks[src]);
},
addAudioSample: async (src, sample) => {
const callback = audioCallbacks[src];
if (!callback) {
throw new Error('No callback found for audio sample');
}
latestSample[src] = sample.decodingTimestamp;
await callback(sample);
},
addVideoSample: async (src, sample) => {
const callback = videoCallbacks[src];
if (!callback) {
throw new Error('No callback found for video sample.');
}
latestSample[src] = sample.decodingTimestamp;
await callback(sample);
},
getNextStreamToRun: (streams) => {
var _a, _b, _c;
for (const stream of streams) {
if (getAllChunksProcessedForPlaylist(stream)) {
continue;
}
// If a stream does not have a track yet, work on that
if (!streamsWithTracks.includes(stream)) {
log_1.Log.trace(logLevel, `Did not yet detect track of ${stream}, working on that`);
return stream;
}
}
let smallestDts = Infinity;
for (const stream of streams) {
if (getAllChunksProcessedForPlaylist(stream)) {
continue;
}
if (((_a = latestSample[stream]) !== null && _a !== void 0 ? _a : 0) < smallestDts) {
smallestDts = (_b = latestSample[stream]) !== null && _b !== void 0 ? _b : 0;
}
}
for (const stream of streams) {
if (getAllChunksProcessedForPlaylist(stream)) {
continue;
}
if (((_c = latestSample[stream]) !== null && _c !== void 0 ? _c : 0) === smallestDts) {
log_1.Log.trace(logLevel, `Working on ${stream} because it has the smallest DTS`);
return stream;
}
}
throw new Error('should be done with parsing now');
},
};
};
exports.sampleSorter = sampleSorter;

View File

@@ -0,0 +1,5 @@
import type { M3uChunk } from '../get-chunks';
export declare const getChunkToSeekTo: ({ chunks, seekToSecondsToProcess, }: {
chunks: M3uChunk[];
seekToSecondsToProcess: number;
}) => number;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChunkToSeekTo = void 0;
const getChunkToSeekTo = ({ chunks, seekToSecondsToProcess, }) => {
let duration = 0;
for (let i = 0; i < chunks.length; i++) {
if (duration >= seekToSecondsToProcess) {
return Math.max(0, i - 1);
}
duration += chunks[i].duration;
}
return Math.max(0, chunks.length - 1);
};
exports.getChunkToSeekTo = getChunkToSeekTo;

View File

@@ -0,0 +1,2 @@
import type { M3u8SeekingHints } from '../../seeking-hints';
export declare const getSeekingHintsForM3u: () => M3u8SeekingHints;

View File

@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSeekingHintsForM3u = void 0;
const getSeekingHintsForM3u = () => {
return {
type: 'm3u8-seeking-hints',
};
};
exports.getSeekingHintsForM3u = getSeekingHintsForM3u;

View File

@@ -0,0 +1,20 @@
import type { M3uAssociatedPlaylist, M3uStream } from './get-streams';
export type SelectM3uStreamFnOptions = {
streams: M3uStream[];
};
export type SelectM3uStreamFn = (options: SelectM3uStreamFnOptions) => number | Promise<number>;
export type SelectM3uAssociatedPlaylistsFnOptions = {
associatedPlaylists: M3uAssociatedPlaylist[];
};
export type SelectM3uAssociatedPlaylistsFn = (options: SelectM3uAssociatedPlaylistsFnOptions) => M3uAssociatedPlaylist[] | Promise<M3uAssociatedPlaylist[]>;
export declare const selectAssociatedPlaylists: ({ playlists, fn, skipAudioTracks, }: {
playlists: M3uAssociatedPlaylist[];
fn: SelectM3uAssociatedPlaylistsFn;
skipAudioTracks: boolean;
}) => Promise<M3uAssociatedPlaylist[]>;
export declare const defaultSelectM3uAssociatedPlaylists: SelectM3uAssociatedPlaylistsFn;
export declare const selectStream: ({ streams, fn, }: {
streams: M3uStream[];
fn: SelectM3uStreamFn;
}) => Promise<M3uStream>;
export declare const defaultSelectM3uStreamFn: SelectM3uStreamFn;

View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultSelectM3uStreamFn = exports.selectStream = exports.defaultSelectM3uAssociatedPlaylists = exports.selectAssociatedPlaylists = void 0;
const selectAssociatedPlaylists = async ({ playlists, fn, skipAudioTracks, }) => {
if (playlists.length < 1) {
return Promise.resolve([]);
}
const streams = await fn({ associatedPlaylists: playlists });
if (!Array.isArray(streams)) {
throw new Error('Expected an array of associated playlists');
}
const selectedStreams = [];
for (const stream of streams) {
if (stream.isAudio && skipAudioTracks) {
continue;
}
if (!playlists.find((playlist) => playlist.src === stream.src)) {
throw new Error(`The associated playlist ${JSON.stringify(streams)} cannot be selected because it was not in the list of selectable playlists`);
}
selectedStreams.push(stream);
}
return selectedStreams;
};
exports.selectAssociatedPlaylists = selectAssociatedPlaylists;
const defaultSelectM3uAssociatedPlaylists = ({ associatedPlaylists }) => {
if (associatedPlaylists.length === 1) {
return associatedPlaylists;
}
return associatedPlaylists.filter((playlist) => playlist.default);
};
exports.defaultSelectM3uAssociatedPlaylists = defaultSelectM3uAssociatedPlaylists;
const selectStream = async ({ streams, fn, }) => {
if (streams.length < 1) {
throw new Error('No streams found');
}
const selectedStreamId = await fn({ streams });
const selectedStream = streams.find((stream) => stream.id === selectedStreamId);
if (!selectedStream) {
throw new Error(`No stream with the id ${selectedStreamId} found`);
}
return Promise.resolve(selectedStream);
};
exports.selectStream = selectStream;
const defaultSelectM3uStreamFn = ({ streams }) => {
return Promise.resolve(streams[0].id);
};
exports.defaultSelectM3uStreamFn = defaultSelectM3uStreamFn;

View File

@@ -0,0 +1,88 @@
import type { ParseMediaSrc } from '../../options';
export type M3uHeader = {
type: 'm3u-header';
};
export type M3uVersion = {
type: 'm3u-version';
version: string;
};
export type M3uIndependentSegments = {
type: 'm3u-independent-segments';
};
export type M3uMedia = {
type: 'm3u-media';
};
export type M3uTargetDuration = {
type: 'm3u-target-duration';
duration: number;
};
export type M3uPlaylistType = {
type: 'm3u-playlist-type';
playlistType: string;
};
export type M3uPlaylist = {
type: 'm3u-playlist';
boxes: M3uBox[];
src: ParseMediaSrc;
};
export type M3uExtInf = {
type: 'm3u-extinf';
value: number;
};
export type M3uEndList = {
type: 'm3u-endlist';
};
export type M3uMediaSequence = {
type: 'm3u-media-sequence';
value: number;
};
export type M3uDiscontinuitySequence = {
type: 'm3u-discontinuity-sequence';
value: number;
};
export type M3uIFrameStreamInfo = {
type: 'm3u-i-frame-stream-info';
};
export type M3uMap = {
type: 'm3u-map';
value: string;
};
export type M3uStreamInfo = {
type: 'm3u-stream-info';
bandwidthInBitsPerSec: number | null;
averageBandwidthInBitsPerSec: number | null;
codecs: string[] | null;
dimensions: {
width: number;
height: number;
} | null;
audio: string | null;
};
export type M3uMediaInfo = {
type: 'm3u-media-info';
groupId: string;
language: string | null;
name: string | null;
autoselect: boolean;
default: boolean;
channels: number | null;
uri: string;
mediaType: string | null;
};
export type M3uTextValue = {
type: 'm3u-text-value';
value: string;
};
export type M3uAllowCache = {
type: 'm3u-allow-cache';
allowsCache: boolean;
};
export type M3uProgramDateTime = {
type: 'm3u-program-date-time';
dateTime: string;
};
export type M3uBox = M3uHeader | M3uPlaylist | M3uVersion | M3uIndependentSegments | M3uStreamInfo | M3uTargetDuration | M3uPlaylistType | M3uExtInf | M3uMedia | M3uMediaInfo | M3uEndList | M3uMediaSequence | M3uDiscontinuitySequence | M3uMap | M3uIFrameStreamInfo | M3uTextValue | M3uAllowCache | M3uProgramDateTime;
export type M3uStructure = {
type: 'm3u';
boxes: M3uBox[];
};

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });