A BUNCH of HLS (and related) progress

- Add support for fMP4 segments
- Add initInput (supported by ISOBMFF and MPEG-TS)
- Input.isSupported()
- Refactored file size retrieval logic, the file size can now be retrieved alongside the first read.
- Add support for unsized UrlSources
- Add support for multiple trun boxes in a row for the same track (as the spec literally says lol)
- Add support for more M3U8 features
- Fix faulty line reader
- MPEG-TS improvements: support video parameters not in first packet, support better key frame detection, support extension PES packets without PTS
- Fix reorder buffer being 1 element too small
This commit is contained in:
Vanilagy
2026-02-20 19:56:20 +01:00
parent de84d78092
commit 68b3ae7155
73 changed files with 17552 additions and 967 deletions
+40 -1
View File
@@ -11,8 +11,33 @@
document.body.append(fileInput);
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
const input = new Mediabunny.Input({
formats: Mediabunny.ALL_FORMATS,
source: new Mediabunny.BlobSource(file),
});
/*
const manifest = new Mediabunny.ManifestInput({
entryPath: 'https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8'
entryPath: /*'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' ?? 'https://bitmovin-a.akamaihd.net/content/dataset/multi-codec/hevc/v720p_ts.m3u8',
getSource: path => new Mediabunny.UrlSource(path),
manifestFormats: Mediabunny.ALL_MANIFEST_FORMATS,
mediaFormats: Mediabunny.ALL_FORMATS,
});
const variants = await manifest.getVariants();
const variant = variants[0];
const input = variant.toInput();
*/
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
console.log(await sink.getPacket(17.48808888888889));
/*
const manifest = new Mediabunny.ManifestInput({
entryPath: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8'
?? 'https://playertest.longtailvideo.com/adaptive/vod-with-mp3/manifest.m3u8'
?? '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'
@@ -27,6 +52,19 @@
});
const variants = await manifest.getVariants();
const variant = variants[0];
const firstSegment = await variant.getFirstSegment();
const input = await firstSegment.toInput();
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
console.log(packet.byteLength, packet.timestamp, packet.duration)
}
*/
/*
return;
const variant = variants[2];//variants.find(x => x.path.endsWith('aac/und/stream.m3u8'));
const input = variant.toInput();
const audioTrack = await input.getPrimaryAudioTrack();
@@ -35,6 +73,7 @@
for await (const packet of sink.packets()) {
console.log(packet.timestamp, packet.duration)
}
*/
/*
const file = fileInput.files[0];
+2 -2
View File
@@ -31,11 +31,11 @@
</a>
</div>
<p class="text-xs opacity-60 mt-2" id="file-name"></p>
<p class="text-xs opacity-60 mt-2 mx-auto text-center max-w-[36rem]" id="file-name"></p>
<hr class="w-full max-w-96 my-4 border-zinc-300 dark:border-zinc-700" style="display: none;">
<p id="error-element" class="text-red-500"></p>
<p id="error-element" class="text-red-500 mx-auto text-center max-w-[36rem]"></p>
<div class="w-full max-w-80 h-2 rounded-full bg-zinc-200 dark:bg-zinc-750 overflow-hidden" id="progress-bar-container" style="display: none;">
<div class="h-full bg-emerald-500 w-0" id="progress-bar"></div>
+2 -2
View File
@@ -21,8 +21,8 @@
<hr class="w-full max-w-96 my-4 border-zinc-300 dark:border-zinc-700" style="display: none;">
<p id="error-element" class="text-red-500"></p>
<p id="warning-element" class="text-amber-500"></p>
<p id="error-element" class="text-red-500 mx-auto text-center max-w-[36rem]"></p>
<p id="warning-element" class="text-amber-500 mx-auto text-center max-w-[36rem]"></p>
<div class="flex gap-4" id="main-container" style="display: none;">
<div class="flex flex-col items-center">
+4 -4
View File
@@ -31,13 +31,13 @@
</a>
</div>
<p class="text-xs opacity-60 mt-2" id="file-name"></p>
<p class="text-xs opacity-60 mt-2 mx-auto text-center max-w-[36rem]" id="file-name"></p>
<hr class="w-full max-w-96 my-4 border-zinc-300 dark:border-zinc-700" style="display: none;">
<p id="error-element" class="text-red-500"></p>
<p id="warning-element" class="text-amber-500 mb-1"></p>
<p id="loading-element" class="text-sm animate-pulse text-zinc-500" style="display: none;">Loading...</p>
<p id="error-element" class="text-red-500 mx-auto text-center max-w-[36rem]"></p>
<p id="warning-element" class="text-amber-500 mb-1 mx-auto text-center max-w-[36rem]"></p>
<p id="loading-element" class="text-sm animate-pulse text-zinc-500 mx-auto text-center max-w-[36rem]" style="display: none;">Loading...</p>
<div id="player" class="relative bg-black rounded-xl shrink min-h-14 min-w-0 w-full max-w-5xl overflow-hidden select-none" style="display: none;">
<canvas class="size-full object-contain" width="1280" height="720"></canvas>
+3 -3
View File
@@ -92,16 +92,16 @@ const initMediaPlayer = async (resource: File | string) => {
warningElement.textContent = '';
let input: Input;
if (typeof resource === 'string' && resource.endsWith('.m3u8')) {
if (typeof resource === 'string' && resource.includes('.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]!;
// const variant = (await manifestInput.getVariants())[0]!;
input = variant.toInput();// await manifestInput.toInput();
input = 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
+1 -1
View File
@@ -31,7 +31,7 @@
</a>
</div>
<p class="text-xs opacity-60 mt-2" id="file-name"></p>
<p class="text-xs opacity-60 mt-2 mx-auto text-center max-w-[36rem]" id="file-name"></p>
<hr class="w-full max-w-96 my-4 border-zinc-300 dark:border-zinc-700" style="display: none;">
+1 -1
View File
@@ -35,7 +35,7 @@
<hr class="w-full max-w-96 my-6 border-zinc-300 dark:border-zinc-700" style="display: none;">
<p id="error-element" class="text-red-500"></p>
<p id="error-element" class="text-red-500 mx-auto text-center max-w-[36rem]"></p>
<div class="w-full max-w-80 h-2 rounded-full bg-zinc-200 dark:bg-zinc-750 overflow-hidden" id="progress-bar-container" style="display: none;">
<div class="h-full bg-pink-500 w-0" id="progress-bar"></div>
+2 -2
View File
@@ -31,11 +31,11 @@
</a>
</div>
<p class="text-xs opacity-60 mt-2" id="file-name"></p>
<p class="text-xs opacity-60 mt-2 mx-auto text-center max-w-[36rem]" id="file-name"></p>
<hr class="w-full max-w-96 my-4 border-gray-300 dark:border-zinc-700" style="display: none;">
<p id="error-element" class="text-red-500"></p>
<p id="error-element" class="text-red-500 mx-auto text-center max-w-[36rem]"></p>
<div id="thumbnail-container" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"></div>
<a href="/" class="fixed top-0 left-0 flex gap-2 py-2 px-5 items-center">
+8 -1
View File
@@ -36,7 +36,14 @@ export class InputAggregateDemuxer extends Demuxer {
getTracks() {
return this.tracksPromise ??= (async () => {
const subInputTracks = await Promise.all(this.subInputs.map(x => x.getTracks()));
const subInputTracks = await Promise.all(this.subInputs.map(async (input) => {
const supported = await input.isSupported();
if (!supported) {
return [];
}
return input.getTracks();
}));
const tracks: InputTrack[] = [];
for (const inputTracks of subInputTracks) {
+4
View File
@@ -17,6 +17,10 @@ export abstract class Demuxer {
this.input = input;
}
async isSupported() {
return true;
}
abstract getTracks(): Promise<InputTrack[]>;
abstract getMimeType(): Promise<string>;
abstract getMetadataTags(): Promise<MetadataTags>;
+5 -1
View File
@@ -66,7 +66,11 @@ export abstract class IsobmffInputFormat extends InputFormat {
slice.skip(4);
const fourCc = readAscii(slice, 4);
if (fourCc !== 'ftyp') {
if (
fourCc !== 'ftyp'
&& fourCc !== 'styp' // Segment
&& fourCc !== 'moof' // Not a legal segment, but seen in practice (sigh)
) {
return null;
}
+24 -3
View File
@@ -14,6 +14,8 @@ import { Source } from './source';
polyfillSymbolDispose();
const UNSUPPORTED_INPUT_FORMAT_MESSAGE = 'Input has an unsupported or unrecognizable format.';
/**
* The options for creating an Input object.
* @group Input files & tracks
@@ -24,6 +26,7 @@ export type InputOptions<S extends Source = Source> = {
formats: InputFormat[];
/** The source from which data will be read. */
source: S;
initInput?: Input;
};
/**
@@ -37,6 +40,8 @@ export class Input<S extends Source = Source> implements Disposable {
/** @internal */
_formats: InputFormat[];
/** @internal */
_initInput: Input | null;
/** @internal */
_demuxerPromise: Promise<Demuxer> | null = null;
/** @internal */
_format: InputFormat | null = null;
@@ -67,15 +72,19 @@ export class Input<S extends Source = Source> implements Disposable {
if (options.source._disposed) {
throw new TypeError('options.source must not be disposed.');
}
if (options.initInput !== undefined && !(options.initInput instanceof Input)) {
throw new TypeError('options.initInput, when provided, must be an Input.');
}
this._formats = options.formats;
this._source = options.source;
this._initInput = options.initInput ?? null;
}
/** @internal */
_getDemuxer() {
return this._demuxerPromise ??= (async () => {
this._reader = await Reader.fromSource(this._source);
this._reader = new Reader(this._source);
for (const format of this._formats) {
const canRead = await format._canReadInput(this);
@@ -85,8 +94,7 @@ export class Input<S extends Source = Source> implements Disposable {
}
}
console.log(this._source);
throw new Error('Input has an unsupported or unrecognizable format.');
throw new Error(UNSUPPORTED_INPUT_FORMAT_MESSAGE);
})();
}
@@ -109,6 +117,19 @@ export class Input<S extends Source = Source> implements Disposable {
return this._format;
}
async isSupported(): Promise<boolean> {
try {
const demuxer = await this._getDemuxer();
return demuxer.isSupported();
} catch (error) {
if (error instanceof Error && error.message === UNSUPPORTED_INPUT_FORMAT_MESSAGE) {
return false;
}
throw error;
}
}
/**
* Computes the duration of the input file, in seconds. More precisely, returns the largest end timestamp among
* all tracks.
+145 -51
View File
@@ -105,7 +105,7 @@ type InternalTrack = {
internalCodecId: string | null;
name: string | null;
languageCode: string;
sampleTableByteOffset: number;
sampleTableByteOffset: number | null; // null when the track's sample table is another file (ominous ik 👀)
sampleTable: SampleTable | null;
fragmentLookupTable: FragmentLookupTableEntry[];
currentFragmentState: FragmentTrackState | null;
@@ -211,6 +211,11 @@ type FragmentTrackState = {
type FragmentTrackData = {
track: InternalTrack;
// Kept as state for the presence of multiple trun boxes
currentTimestamp: number;
currentOffset: number;
startTimestamp: number;
endTimestamp: number;
firstKeyFrameTimestamp: number | null;
@@ -291,6 +296,8 @@ export class IsobmffDemuxer extends Demuxer {
readMetadata() {
return this.metadataPromise ??= (async () => {
let currentPos = 0;
let lookForMfraBox = false;
while (true) {
let slice = this.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
if (slice instanceof Promise) slice = await slice;
@@ -302,7 +309,7 @@ export class IsobmffDemuxer extends Demuxer {
break;
}
if (boxInfo.name === 'ftyp') {
if (boxInfo.name === 'ftyp' || boxInfo.name === 'styp') {
const majorBrand = readAscii(slice, 4);
this.isQuickTime = majorBrand === 'qt ';
} else if (boxInfo.name === 'moov') {
@@ -326,13 +333,87 @@ export class IsobmffDemuxer extends Demuxer {
track.editListOffset -= Math.round(previousSegmentDurationsInSeconds * track.timescale);
}
lookForMfraBox = this.isFragmented
&& this.reader.fileSize !== null
&& this.reader.fileSize > startPos + boxInfo.totalSize; // There's more after the moov box
break;
} else if (boxInfo.name === 'moof') {
if (!this.input._initInput) {
throw new Error(
'"moof" box encountered with no "moov" box present; this file is likely a Segment as'
+ ' described in ISO/IEC 14496-12 Section 8.16. A separate init file that contains a "moov"'
+ ' box is required to read this file, please provide it using InputOptions.initInput.',
);
}
const initDemuxer = (await this.input._initInput._getDemuxer()) as IsobmffDemuxer;
if (initDemuxer.constructor !== IsobmffDemuxer) {
throw new Error('Init input must match the input\'s format.');
}
await initDemuxer.readMetadata();
this.movieTimescale = initDemuxer.movieTimescale;
this.movieDurationInTimescale = initDemuxer.movieDurationInTimescale;
this.metadataTags = initDemuxer.metadataTags;
this.isFragmented = true;
this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
// Create tracks from the init input's tracks
for (const foreignTrack of initDemuxer.tracks) {
const track: InternalTrack = {
id: foreignTrack.id,
demuxer: this,
inputTrack: null,
disposition: foreignTrack.disposition,
timescale: foreignTrack.timescale,
durationInMediaTimescale: foreignTrack.durationInMediaTimescale,
durationInMovieTimescale: foreignTrack.durationInMovieTimescale,
rotation: foreignTrack.rotation,
internalCodecId: foreignTrack.internalCodecId,
name: foreignTrack.name,
languageCode: foreignTrack.languageCode,
sampleTableByteOffset: null,
sampleTable: null,
fragmentLookupTable: [],
currentFragmentState: null,
fragmentPositionCache: [],
editListPreviousSegmentDurations: foreignTrack.editListPreviousSegmentDurations,
editListOffset: foreignTrack.editListOffset,
info: foreignTrack.info,
};
if (foreignTrack.inputTrack) {
assert(track.info);
if (track.info.type === 'video' && track.info.width !== -1) {
const videoTrack = track as InternalVideoTrack;
track.inputTrack
= new InputVideoTrack(this.input, new IsobmffVideoTrackBacking(videoTrack));
this.tracks.push(track);
} else if (track.info.type === 'audio' && track.info.numberOfChannels !== -1) {
const audioTrack = track as InternalAudioTrack;
track.inputTrack
= new InputAudioTrack(this.input, new IsobmffAudioTrackBacking(audioTrack));
this.tracks.push(track);
}
} else {
// The track didn't have enough info to warrant an input track
}
}
lookForMfraBox = false; // No point in doing it for segment files
break;
}
currentPos = startPos + boxInfo.totalSize;
}
if (this.isFragmented && this.reader.fileSize !== null) {
if (lookForMfraBox) {
assert(this.reader.fileSize !== null);
// The last 4 bytes may contain the size of the mfra box at the end of the file
let lastWordSlice = this.reader.requestSlice(this.reader.fileSize - 4, 4);
if (lastWordSlice instanceof Promise) lastWordSlice = await lastWordSlice;
@@ -384,7 +465,13 @@ export class IsobmffDemuxer extends Demuxer {
};
internalTrack.sampleTable = sampleTable;
if (internalTrack.sampleTableByteOffset === null) {
// There's no sample table to read, it's in another file (happens with segments)
return sampleTable;
}
assert(this.moovSlice);
const stblContainerSlice = this.moovSlice.slice(internalTrack.sampleTableByteOffset);
this.currentTrack = internalTrack;
@@ -1838,6 +1925,35 @@ export class IsobmffDemuxer extends Demuxer {
if (this.currentTrack) {
const trackData = this.currentFragment.trackData.get(this.currentTrack.id);
if (trackData) {
this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
trackData.presentationTimestamps = trackData.samples
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
const currentEntry = trackData.presentationTimestamps[i]!;
const currentSample = trackData.samples[currentEntry.sampleIndex]!;
if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
}
if (i < trackData.presentationTimestamps.length - 1) {
// Update sample durations based on presentation order
const nextEntry = trackData.presentationTimestamps[i + 1]!;
const duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
currentSample.duration = duration;
}
}
const firstSample = trackData.samples[trackData.presentationTimestamps[0]!.sampleIndex]!;
const lastSample = trackData.samples[last(trackData.presentationTimestamps)!.sampleIndex]!;
trackData.startTimestamp = firstSample.presentationTimestamp;
trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
const { currentFragmentState } = this.currentTrack;
assert(currentFragmentState);
@@ -1931,13 +2047,7 @@ export class IsobmffDemuxer extends Demuxer {
assert(this.currentFragment);
assert(track.currentFragmentState);
if (this.currentFragment.trackData.has(track.id)) {
console.warn('Can\'t have two trun boxes for the same track in one fragment. Ignoring...');
break;
}
const version = readU8(slice);
const flags = readU24Be(slice);
const dataOffsetPresent = Boolean(flags & 0x000001);
const firstSampleFlagsPresent = Boolean(flags & 0x000004);
@@ -1948,27 +2058,31 @@ export class IsobmffDemuxer extends Demuxer {
const sampleCount = readU32Be(slice);
let dataOffset = track.currentFragmentState.baseDataOffset;
let dataOffset: number | null = null;
if (dataOffsetPresent) {
dataOffset += readI32Be(slice);
dataOffset = readI32Be(slice);
}
let firstSampleFlags: number | null = null;
if (firstSampleFlagsPresent) {
firstSampleFlags = readU32Be(slice);
}
let currentOffset = dataOffset;
let trackData: FragmentTrackData;
if (sampleCount === 0) {
// Don't associate the fragment with the track if it has no samples, this simplifies other code
this.currentFragment.implicitBaseDataOffset = currentOffset;
break;
if (this.currentFragment.trackData.has(track.id)) {
trackData = this.currentFragment.trackData.get(track.id)!;
if (dataOffset !== null) {
trackData.currentOffset = track.currentFragmentState.baseDataOffset + dataOffset;
} else {
// "If the data-offset is not present, then the data for this run starts immediately after the
// data of the previous run"
}
let currentTimestamp = 0;
const trackData: FragmentTrackData = {
} else {
trackData = {
track,
currentTimestamp: 0,
currentOffset: track.currentFragmentState.baseDataOffset + (dataOffset ?? 0),
startTimestamp: 0,
endTimestamp: 0,
firstKeyFrameTimestamp: null,
@@ -1977,6 +2091,13 @@ export class IsobmffDemuxer extends Demuxer {
startTimestampIsFinal: false,
};
this.currentFragment.trackData.set(track.id, trackData);
}
if (sampleCount === 0) {
// Don't associate the fragment with the track if it has no samples, this simplifies other code
this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
break;
}
for (let i = 0; i < sampleCount; i++) {
let sampleDuration: number;
@@ -2018,43 +2139,16 @@ export class IsobmffDemuxer extends Demuxer {
const isKeyFrame = !(sampleFlags & 0x00010000);
trackData.samples.push({
presentationTimestamp: currentTimestamp + sampleCompositionTimeOffset,
presentationTimestamp: trackData.currentTimestamp + sampleCompositionTimeOffset,
duration: sampleDuration,
byteOffset: currentOffset,
byteOffset: trackData.currentOffset,
byteSize: sampleSize,
isKeyFrame,
});
currentOffset += sampleSize;
currentTimestamp += sampleDuration;
trackData.currentOffset += sampleSize;
trackData.currentTimestamp += sampleDuration;
}
trackData.presentationTimestamps = trackData.samples
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
const currentEntry = trackData.presentationTimestamps[i]!;
const currentSample = trackData.samples[currentEntry.sampleIndex]!;
if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
}
if (i < trackData.presentationTimestamps.length - 1) {
// Update sample durations based on presentation order
const nextEntry = trackData.presentationTimestamps[i + 1]!;
currentSample.duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
}
}
const firstSample = trackData.samples[trackData.presentationTimestamps[0]!.sampleIndex]!;
const lastSample = trackData.samples[last(trackData.presentationTimestamps)!.sampleIndex]!;
trackData.startTimestamp = firstSample.presentationTimestamp;
trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
this.currentFragment.implicitBaseDataOffset = currentOffset;
}; break;
// Metadata section
+141 -44
View File
@@ -9,7 +9,7 @@
import { AES_128_BLOCK_SIZE } from '../aes';
import { ManifestInput } from '../manifest-input';
import { ManifestParser } from '../manifest-parser';
import { ManifestInputVariant } from '../manifest-input-variant';
import { AssociatedGroup, 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';
@@ -18,18 +18,18 @@ import { inferCodecFromCodecString, MediaCodec } from '../codec';
const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
export class M3u8Parser extends ManifestParser {
_metadataPromise: Promise<void> | null = null;
_variants: M3u8ManifestVariant[] = [];
_lineReader: LineReader;
metadataPromise: Promise<void> | null = null;
variants: M3u8ManifestVariant[] = [];
lineReader: LineReader;
constructor(input: ManifestInput) {
super(input);
this._lineReader = new LineReader(() => input._entryReader, isM3u8Comment);
this.lineReader = new LineReader(() => input._entryReader, canIgnoreLine);
}
_readMetadata() {
return this._metadataPromise ??= (async () => {
let line = this._lineReader.readNextLine();
readMetadata() {
return this.metadataPromise ??= (async () => {
let line = this.lineReader.readNextLine();
if (line instanceof Promise) line = await line;
if (line !== '#EXTM3U') {
@@ -39,7 +39,7 @@ export class M3u8Parser extends ManifestParser {
let iFramesOnlyTagFound = false;
while (true) {
let line = this._lineReader.readNextLine();
let line = this.lineReader.readNextLine();
if (line instanceof Promise) line = await line;
if (line === null) {
@@ -47,7 +47,7 @@ export class M3u8Parser extends ManifestParser {
}
if (line.startsWith('#EXT-X-STREAM-INF:')) {
let playlistPath = this._lineReader.readNextLine();
let playlistPath = this.lineReader.readNextLine();
if (playlistPath instanceof Promise) playlistPath = await playlistPath;
if (playlistPath === null) {
@@ -57,13 +57,12 @@ export class M3u8Parser extends ManifestParser {
const fullPath = joinPaths(this._input._entryPath, playlistPath);
const attributes = new AttributeList(line.slice(18));
this._variants.push(new M3u8ManifestVariant(
this,
this.pushOrMergeVariant(
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');
@@ -76,13 +75,12 @@ export class M3u8Parser extends ManifestParser {
const fullPath = joinPaths(this._input._entryPath, playlistPath);
this._variants.push(new M3u8ManifestVariant(
this,
this.pushOrMergeVariant(
fullPath,
null,
attributes,
true,
));
);
} else if (line.startsWith('#EXT-X-MEDIA:')) {
const attributes = new AttributeList(line.slice(13));
@@ -100,23 +98,22 @@ export class M3u8Parser extends ManifestParser {
const fullPath = joinPaths(this._input._entryPath, uri);
this._variants.push(new M3u8ManifestVariant(
this,
this.pushOrMergeVariant(
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 = [
this.variants = [
new M3u8ManifestVariant(
this,
this._input._entryPath,
this._lineReader.reader,
this.lineReader.reader,
new AttributeList(''),
iFramesOnlyTagFound,
),
@@ -128,9 +125,32 @@ export class M3u8Parser extends ManifestParser {
})();
}
pushOrMergeVariant(
path: string,
reader: Reader | null,
attributes: AttributeList,
isKeyFrameOnly: boolean,
) {
const existing = this.variants.find(v => v.path === path);
if (existing) {
// Sometimes the same path exists multiple times, so let's just aggregate the data then
// (instead of showing the variant twice)
existing._attributes.merge(attributes);
existing._isKeyFrameOnly = isKeyFrameOnly;
} else {
this.variants.push(new M3u8ManifestVariant(
this,
path,
reader,
attributes,
isKeyFrameOnly,
));
}
}
override async getVariants() {
await this._readMetadata();
return this._variants;
await this.readMetadata();
return this.variants;
}
}
@@ -147,6 +167,7 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
_mutex = new AsyncMutex();
_currentKey: SegmentEncryptionInfo | null = null;
_nextSequenceNumber = 0;
_currentFirstSegment: ManifestInputSegment | null = null;
_currentInitSegment: ManifestInputSegment | null = null;
_lastByteRangeEnd: number | null = null;
_nextByteRange: { offset: number; length: number } | null = null;
@@ -166,12 +187,12 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
this._parser = parser;
if (reader) {
this._lineReader = new LineReader(() => reader, isM3u8Comment);
this._lineReader = new LineReader(() => reader, canIgnoreLine);
} else {
this._lineReader = new LineReader(async () => {
const source = await this.input._getSourceUncached(this.path);
return Reader.fromSource(source);
}, isM3u8Comment);
return new Reader(source);
}, canIgnoreLine);
}
}
@@ -195,11 +216,30 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
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');
get associatedGroups() {
const groups: AssociatedGroup[] = [];
const videoGroupId = this._attributes.get('video');
if (videoGroupId) {
groups.push({ id: videoGroupId, type: 'video' });
}
const audioGroupId = this._attributes.get('audio');
if (audioGroupId) {
groups.push({ id: audioGroupId, type: 'audio' });
}
const subtitlesGroupId = this._attributes.get('subtitles');
if (subtitlesGroupId) {
groups.push({ id: subtitlesGroupId, type: 'subtitles' });
}
const closedCaptionsGroupId = this._attributes.get('closed-captions');
if (closedCaptionsGroupId) {
groups.push({ id: closedCaptionsGroupId, type: 'closed-captions' });
}
return groups;
}
_getCodecStrings() {
@@ -338,20 +378,22 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
this._nextSegmentDuration,
this._nextSegmentTitle,
key,
this._currentFirstSegment,
this._currentInitSegment,
);
this._segments.push(segment);
this._accumulatedTime += this._nextSegmentDuration;
this._nextSequenceNumber++;
this._currentInitSegment ??= segment;
this._currentFirstSegment ??= segment;
this._nextSegmentDuration = null;
this._nextSegmentTitle = null;
this._lastByteRangeEnd = this._nextByteRange
? this._nextByteRange.offset + this._nextByteRange.length
: null;
if (this._nextByteRange === null) {
this._lastByteRangeEnd = null;
} else {
this._nextByteRange = null;
}
return;
}
@@ -368,6 +410,52 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
this._nextSegmentDuration = duration;
this._nextSegmentTitle = title;
} else if (line.startsWith('#EXT-X-MAP:')) {
const attributes = new AttributeList(line.slice(11));
const uri = attributes.get('uri');
if (!uri) {
throw new Error('Invalid #EXT-X-MAP tag; missing URI attribute.');
}
const byteRange = attributes.get('byterange');
if (byteRange !== null) {
this._parseAndUpdateByteRange(byteRange);
}
const fullPath = joinPaths(this.path, uri);
const location: ManifestInputSegmentLocation = {
path: fullPath,
offset: this._nextByteRange?.offset ?? 0,
length: this._nextByteRange?.length ?? null,
};
if (this._currentKey?.method === 'AES-128' && !this._currentKey.iv) {
// Required by the spec
throw new Error('IV attribute must be set on #EXT-X-KEY tag preceding the #EXT-X-MAP tag.');
}
const segment = new ManifestInputSegment(
this,
location,
this._accumulatedTime,
0,
null,
this._currentKey,
null,
null,
);
// Accumulated time and sequence number are not updated in this case
this._currentInitSegment = segment;
this._nextSegmentDuration = null;
this._nextSegmentTitle = null;
if (this._nextByteRange === null) {
this._lastByteRangeEnd = null;
} else {
this._nextByteRange = null;
}
} else if (line.startsWith('#EXT-X-KEY:')) {
const attributes = new AttributeList(line.slice(11));
const method = attributes.get('method');
@@ -416,7 +504,18 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
this._nextSequenceNumber = number;
} else if (line.startsWith('#EXT-X-BYTERANGE:')) {
const content = line.slice(17);
this._parseAndUpdateByteRange(line.slice(17));
} else if (line.startsWith('#EXT-X-DISCONTINUITY')) {
this._currentFirstSegment = null;
this._currentInitSegment = null;
}
}
} finally {
release();
}
}
_parseAndUpdateByteRange(content: string) {
const atIndex = content.indexOf('@');
const length = Number(atIndex === -1 ? content : content.slice(0, atIndex));
@@ -440,13 +539,7 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
}
this._nextByteRange = { offset, length };
} else if (line.startsWith('#EXT-X-DISCONTINUITY')) {
this._currentInitSegment = null;
}
}
} finally {
release();
}
this._lastByteRangeEnd = offset + length;
}
async _readUntilSegmentAt(relativeTimestamp: number) {
@@ -461,7 +554,7 @@ export class M3u8ManifestVariant extends ManifestInputVariant {
}
}
const isM3u8Comment = (line: string) => line.startsWith('#') && !line.startsWith('#EXT');
const canIgnoreLine = (line: string) => line.length === 0 || (line.startsWith('#') && !line.startsWith('#EXT'));
class AttributeList {
_attributes: Record<string, string> = {};
@@ -512,4 +605,8 @@ class AttributeList {
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
merge(other: AttributeList) {
Object.assign(this._attributes, other._attributes);
}
}
+36 -26
View File
@@ -1,7 +1,7 @@
import { AES_128_BLOCK_SIZE, createAesDecryptStream } from './aes';
import { Input } from './input';
import { ManifestInputVariant } from './manifest-input-variant';
import { assert } from './misc';
import { arrayArgmin, assert } from './misc';
import { fs } from './node';
import { readBytes, Reader } from './reader';
import { ReadableStreamSource, Source } from './source';
@@ -26,6 +26,7 @@ export class ManifestInputSegment {
readonly duration: number;
readonly title: string | null;
readonly encryption: SegmentEncryptionInfo | null;
readonly firstSegment: ManifestInputSegment | null;
readonly initSegment: ManifestInputSegment | null;
constructor(
@@ -35,6 +36,7 @@ export class ManifestInputSegment {
duration: number,
title: string | null,
encryption: SegmentEncryptionInfo | null,
firstSegment: ManifestInputSegment | null,
initSegment: ManifestInputSegment | null,
) {
this.variant = variant;
@@ -43,10 +45,23 @@ export class ManifestInputSegment {
this.duration = duration;
this.title = title;
this.encryption = encryption;
this.firstSegment = firstSegment;
this.initSegment = initSegment;
}
async toInput() {
toInput(): Promise<Input> {
const cacheEntry = this.variant._inputCache.find(x => x.segment === this);
if (cacheEntry) {
cacheEntry.age = this.variant._nextInputCacheAge++;
return cacheEntry.inputPromise;
}
const inputPromise = (async () => {
let initInputPromise: Promise<Input> | null = null;
if (this.initSegment || this.firstSegment) {
initInputPromise = (this.initSegment ?? this.firstSegment)!.toInput();
}
let source: Source;
const needsSlice = this.location.offset > 0 || this.location.length !== null;
@@ -65,7 +80,7 @@ export class ManifestInputSegment {
ciphertextSource = ciphertextSource.slice(this.location.offset, this.location.length ?? undefined);
}
const ciphertextReader = await Reader.fromSource(ciphertextSource);
const ciphertextReader = new Reader(ciphertextSource);
const stream = createAesDecryptStream(ciphertextReader, async () => {
const keyReader = await this.variant.input._getEncryptionKeyReader(this.encryption!.keyUri);
@@ -78,35 +93,30 @@ export class ManifestInputSegment {
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);
}
const initInput = await initInputPromise;
return new Input({
source,
formats: this.variant.input._mediaFormats,
initInput: initInput ?? undefined,
});
})();
this.variant._inputCache.push({
segment: this,
inputPromise,
age: this.variant._nextInputCacheAge++,
});
const MAX_INPUT_CACHE_SIZE = 4;
if (this.variant._inputCache.length > MAX_INPUT_CACHE_SIZE) {
const minAgeIndex = arrayArgmin(this.variant._inputCache, x => x.age);
this.variant._inputCache.splice(minAgeIndex, 1);
}
return inputPromise;
}
}
+54 -60
View File
@@ -22,7 +22,7 @@ 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 { arrayCount, assert, Rotation } from './misc';
import { EncodedPacket } from './packet';
import { NullSource } from './source';
@@ -37,10 +37,24 @@ export type ManifestInputVariantMetadata = {
isKeyFrameOnly: boolean;
};
export type AssociatedGroup = {
id: string;
type: 'video' | 'audio' | 'subtitles' | 'closed-captions';
};
export abstract class ManifestInputVariant {
readonly input: ManifestInput;
readonly path: string;
/** @internal */
_nextInputCacheAge = 0;
/** @internal */
_inputCache: {
segment: ManifestInputSegment;
inputPromise: Promise<Input>; // We store the promise so it's immediately available in the cache
age: number;
}[] = [];
/** @internal */
constructor(input: ManifestInput, path: string) {
this.input = input;
@@ -49,7 +63,7 @@ export abstract class ManifestInputVariant {
abstract get metadata(): ManifestInputVariantMetadata;
abstract get groupId(): string | null;
abstract get associatedGroupId(): string | null;
abstract get associatedGroups(): AssociatedGroup[];
abstract getFirstSegment(): Promise<ManifestInputSegment | null>;
abstract getSegmentAt(timestamp: number): Promise<ManifestInputSegment | null>;
@@ -83,15 +97,7 @@ class ManifestInputVariantDemuxer extends Demuxer {
variant: ManifestInputVariant;
tracksPromise: Promise<InputTrack[]> | null = null;
firstSegment: ManifestInputSegment | null = null;
nextInputCacheAge = 0;
inputCache: {
segment: ManifestInputSegment;
inputPromise: Promise<Input>; // We store the promise so it's immediately available in the cache
age: number;
}[] = [];
initSegmentFirstTimestamps = new WeakMap<ManifestInputSegment, number>();
firstSegmentFirstTimestamps = new WeakMap<ManifestInputSegment, number>();
constructor(input: Input, variant: ManifestInputVariant) {
super(input);
@@ -99,6 +105,16 @@ class ManifestInputVariantDemuxer extends Demuxer {
this.variant = variant;
}
override async isSupported() {
const firstSegment = await this.variant.getFirstSegment();
if (!firstSegment) {
return true; // There's no data but that's supported
}
const input = await firstSegment.toInput();
return input.isSupported();
}
async getMetadataTags(): Promise<MetadataTags> {
return {}; // todo?
}
@@ -114,7 +130,7 @@ class ManifestInputVariantDemuxer extends Demuxer {
return [];
}
const input = await this.retrieveInputForSegment(this.firstSegment);
const input = await this.firstSegment.toInput();
const inputTracks = await input.getTracks();
const tracks: InputTrack[] = [];
@@ -140,60 +156,38 @@ class ManifestInputVariantDemuxer extends Demuxer {
})();
}
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;
const firstSegment = segment.firstSegment ?? segment;
let initSegmentFirstTimestamp: number;
if (this.initSegmentFirstTimestamps.has(initSegment)) {
initSegmentFirstTimestamp = this.initSegmentFirstTimestamps.get(initSegment)!;
let firstSegmentFirstTimestamp: number;
if (this.firstSegmentFirstTimestamps.has(firstSegment)) {
firstSegmentFirstTimestamp = this.firstSegmentFirstTimestamps.get(firstSegment)!;
} else {
const initInput = await this.retrieveInputForSegment(initSegment);
initSegmentFirstTimestamp = await initInput.getFirstTimestamp();
this.initSegmentFirstTimestamps.set(initSegment, initSegmentFirstTimestamp);
const firstInput = await firstSegment.toInput();
firstSegmentFirstTimestamp = await firstInput.getFirstTimestamp();
this.firstSegmentFirstTimestamps.set(firstSegment, firstSegmentFirstTimestamp);
}
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 (firstSegment === segment) {
return firstSegment.relativeTimestamp - firstSegmentFirstTimestamp;
}
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;
}
}
const segmentElapsed = segment.relativeTimestamp - firstSegment.relativeTimestamp;
const inputElapsed = segmentFirstTimestamp - firstSegmentFirstTimestamp;
const difference = inputElapsed - segmentElapsed;
return mediaOffset;
if (Math.abs(difference) <= Math.min(0.25, segmentElapsed)) { // Heuristic
// We're close enough
return firstSegment.relativeTimestamp - firstSegmentFirstTimestamp;
} else {
// Ideally, each segment has absolute timestamps that are relative to some outside clock which is
// consistent across segments. This is often the case, but not always. Either the container format used is
// not timestamped at all (like ADTS), or the segments are just fucky. In this case, use the segment's
// relative timestamp to determine where we are, and completely offset out the segment's input start
// timestamp.
return segment.relativeTimestamp - segmentFirstTimestamp;
}
}
}
@@ -315,7 +309,7 @@ class ManifestInputVariantInputTrackBacking implements InputTrackBacking {
return null;
}
const nextInput = await this.demuxer.retrieveInputForSegment(nextSegment);
const nextInput = await nextSegment.toInput();
const nextTracks = await nextInput.getTracks();
const nextTrack = nextTracks.find(t => t.type === info.track.type && t.number === info.track.number);
@@ -352,7 +346,7 @@ class ManifestInputVariantInputTrackBacking implements InputTrackBacking {
}
while (currentSegment) {
const input = await this.demuxer.retrieveInputForSegment(currentSegment);
const input = await currentSegment.toInput();
const tracks = await input.getTracks();
const track = tracks.find(t => (
t.type === this.firstInputTrack.type && t.number === this.firstInputTrack.number
+2 -2
View File
@@ -89,7 +89,7 @@ export class ManifestInput implements Disposable {
_getParser() {
return this._parserPromise ??= (async () => {
const entrySource = await this._getSourceUncached(this._entryPath);
this._entryReader = await Reader.fromSource(entrySource);
this._entryReader = new Reader(entrySource);
for (const format of this._manifestFormats) {
const canRead = await format._canReadManifestInput(this);
@@ -169,7 +169,7 @@ export class ManifestInput implements Disposable {
}
cachedEntry = Promise.resolve(this._getSourceUncached(path))
.then(keySource => Reader.fromSource(keySource));
.then(keySource => new Reader(keySource));
this._encryptionKeyReaders.set(path, cachedEntry);
return cachedEntry;
+19 -2
View File
@@ -132,7 +132,7 @@ export class EncodedPacketSink {
/**
* Retrieves the track's first packet (in decode order), or null if it has no packets. The first packet is very
* likely to be a key packet.
* likely to be a key packet, but it doesn't have to be.
*/
getFirstPacket(options: PacketRetrievalOptions = {}) {
validatePacketRetrievalOptions(options);
@@ -144,6 +144,23 @@ export class EncodedPacketSink {
return maybeFixPacketType(this._track, this._track._backing.getFirstPacket(options), options);
}
/** Retrieves the track's first key packet (in decode order), or null if it has no key packets. */
async getFirstKeyPacket(options: PacketRetrievalOptions = {}) {
validatePacketRetrievalOptions(options);
const firstPacket = await this.getFirstPacket(options);
if (!firstPacket) {
return null;
}
if (firstPacket.type === 'key') {
// Great
return firstPacket;
}
return this.getNextKeyPacket(firstPacket, options);
}
/**
* Retrieves the packet corresponding to the given timestamp, in seconds. More specifically, returns the last packet
* (in presentation order) with a start timestamp less than or equal to the given timestamp. This method can be
@@ -476,7 +493,7 @@ export abstract class BaseMediaSampleSink<
const packetSink = this._createPacketSink();
const keyPacket = await packetSink.getKeyPacket(startTimestamp, { verifyKeyPackets: true })
?? await packetSink.getFirstPacket();
?? await packetSink.getFirstKeyPacket();
let currentPacket: EncodedPacket | null = keyPacket;
File diff suppressed because it is too large Load Diff
+25 -27
View File
@@ -8,18 +8,22 @@
import { InputDisposedError } from './input';
import { assert, clamp, getUint24, MaybePromise, toDataView } from './misc';
import { Source } from './source';
import { DEFAULT_MAX_READ_POSITION, DEFAULT_MIN_READ_POSITION, Source } from './source';
export class Reader {
fileSize!: number | null;
constructor(public source: Source) {}
private constructor(public source: Source) {}
get fileSize(): number | null {
const size = this.source._getFileSize();
if (size === undefined) {
throw new Error('Reading file size too early; read required first.');
}
static async fromSource(source: Source) {
const reader = new this(source);
reader.fileSize = await source.getSizeOrNull();
return size;
}
return reader;
get fileSizeNonStrict() {
return this.source._getFileSize() ?? null;
}
requestSlice(start: number, length: number): MaybePromise<FileSlice | null> {
@@ -31,12 +35,12 @@ export class Reader {
return null;
}
if (this.fileSize !== null && start + length > this.fileSize) {
if (this.fileSizeNonStrict != null && start + length > this.fileSizeNonStrict) {
return null;
}
const end = start + length;
const result = this.source._read(start, end);
const result = this.source._read(start, end, DEFAULT_MIN_READ_POSITION, DEFAULT_MAX_READ_POSITION);
if (result instanceof Promise) {
return result.then((x) => {
@@ -64,10 +68,10 @@ export class Reader {
return null;
}
if (this.fileSize !== null) {
if (this.fileSizeNonStrict != null) {
return this.requestSlice(
start,
clamp(this.fileSize - start, minLength, maxLength),
clamp(this.fileSizeNonStrict - start, minLength, maxLength),
);
} else {
const promisedAttempt = this.requestSlice(start, maxLength);
@@ -77,23 +81,15 @@ export class Reader {
return attempt;
}
const handleFileSize = (fileSize: number | null) => {
assert(fileSize !== null); // The slice couldn't fit, meaning we must know the file size now
// The slice couldn't fit, meaning we must know the file size now
assert(this.fileSizeNonStrict != null);
return this.requestSlice(
start,
clamp(fileSize - start, minLength, maxLength),
clamp(this.fileSizeNonStrict - start, minLength, maxLength),
);
};
const promisedFileSize = this.source._retrieveSize();
if (promisedFileSize instanceof Promise) {
return promisedFileSize.then(handleFileSize);
} else {
return handleFileSize(promisedFileSize);
}
};
if (promisedAttempt instanceof Promise) {
return promisedAttempt.then(handleAttempt);
} else {
@@ -339,16 +335,16 @@ export const readAscii = (slice: FileSlice, length: number) => {
export class LineReader {
getReader: () => MaybePromise<Reader>;
isComment?: (line: string) => boolean;
ignore?: (line: string) => boolean;
reader: Reader | null = null;
textDecoder = new TextDecoder();
readPos = 0;
reachedEnd = false;
lineBuffer = '';
constructor(getReader: () => MaybePromise<Reader>, isComment?: (line: string) => boolean) {
constructor(getReader: () => MaybePromise<Reader>, ignore?: (line: string) => boolean) {
this.getReader = getReader;
this.isComment = isComment;
this.ignore = ignore;
}
readNextLine(): MaybePromise<string | null> {
@@ -375,7 +371,9 @@ export class LineReader {
if (!slice || slice.length === 0) {
this.reachedEnd = true;
return null;
const line = this.lineBuffer.trim();
return line || null;
}
const bytes = readBytes(slice, slice.length);
@@ -403,7 +401,7 @@ export class LineReader {
const line = this.lineBuffer.slice(0, newlineIndex).trim();
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
if (this.isComment?.(line)) {
if (this.ignore?.(line)) {
continue;
}
+242 -133
View File
@@ -35,6 +35,9 @@ export type ReadResult = {
offset: number;
};
export const DEFAULT_MIN_READ_POSITION = 0;
export const DEFAULT_MAX_READ_POSITION = Infinity;
/**
* The source base class, representing a resource from which bytes can be read.
* @group Input sources
@@ -42,9 +45,14 @@ export type ReadResult = {
*/
export abstract class Source {
/** @internal */
abstract _retrieveSize(): MaybePromise<number | null>;
abstract _getFileSize(): number | null | undefined;
/** @internal */
abstract _read(start: number, end: number): MaybePromise<ReadResult | null>;
abstract _read(
start: number,
end: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null>;
/** @internal */
abstract _dispose(): void;
/** @internal */
@@ -64,7 +72,18 @@ export abstract class Source {
throw new InputDisposedError();
}
return this._sizePromise ??= Promise.resolve(this._retrieveSize());
return this._sizePromise ??= (async () => {
let size = this._getFileSize();
if (size !== undefined) {
return size;
}
await this._read(0, 1, DEFAULT_MIN_READ_POSITION, DEFAULT_MAX_READ_POSITION);
size = this._getFileSize();
assert(size !== undefined);
return size;
})();
}
/**
@@ -134,7 +153,7 @@ export class BufferSource extends Source {
}
/** @internal */
_retrieveSize(): number {
_getFileSize(): number {
return this._bytes.byteLength;
}
@@ -207,19 +226,23 @@ export class BlobSource extends Source {
runWorker: this._runWorker.bind(this),
prefetchProfile: PREFETCH_PROFILES.fileSystem,
});
this._orchestrator.fileSize = blob.size;
}
/** @internal */
_retrieveSize(): number {
const size = this._blob.size;
this._orchestrator.fileSize = size;
return size;
_getFileSize(): number {
return this._orchestrator.fileSize!; // Faster than blob.size
}
/** @internal */
_read(start: number, end: number): MaybePromise<ReadResult> {
return this._orchestrator.read(start, end);
_read(
start: number,
end: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null> {
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
}
/** @internal */
@@ -227,6 +250,8 @@ export class BlobSource extends Source {
/** @internal */
private async _runWorker(worker: ReadWorker) {
assert(worker.strictTarget);
let reader = this._readers.get(worker);
if (reader === undefined) {
// https://github.com/Vanilagy/mediabunny/issues/184
@@ -252,7 +277,7 @@ export class BlobSource extends Source {
if (reader) {
const { done, value } = await reader.read();
if (done) {
this._orchestrator.forgetWorker(worker);
this._orchestrator.onWorkerFinished(worker);
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
}
@@ -327,6 +352,8 @@ const DEFAULT_RETRY_DELAY
return Math.min(2 ** (previousAttempts - 2), 16);
}) satisfies UrlSourceOptions['getRetryDelay'];
const warnedOrigins = new Set<string>();
/**
* Options for {@link UrlSource}.
* @group Input sources
@@ -380,11 +407,12 @@ export class UrlSource extends Source {
_options: UrlSourceOptions;
/** @internal */
_orchestrator: ReadOrchestrator;
/** @internal */
_existingResponses = new WeakMap<ReadWorker, {
response: Response;
abortController: AbortController;
}>();
/**
* Note that this value being true does NOT mean the file size can't change anymore; it just signals that we have at
* least checked if we know the file size or not.
* @internal
*/
_fileSizeDetermined = false;
/**
* Creates a new {@link UrlSource} backed by the resource at the specified URL.
@@ -445,88 +473,33 @@ export class UrlSource extends Source {
}
/** @internal */
async _retrieveSize(): Promise<number> {
// Retrieving the resource size for UrlSource is optimized: Almost always (= always), the first bytes we have to
// read are the start of the file. This means it's smart to combine size fetching with fetching the start of the
// file. We additionally use this step to probe if the server supports range requests, killing three birds with
// one stone.
const abortController = new AbortController();
const response = await retriedFetch(
this._options.fetchFn ?? fetch,
this._url,
mergeRequestInit(this._options.requestInit ?? {}, {
headers: {
// We could also send a non-range request to request the same bytes (all of them), but doing it like
// this is an easy way to check if the server supports range requests in the first place
Range: 'bytes=0-',
},
signal: abortController.signal,
}),
this._getRetryDelay,
() => this._disposed,
);
if (!response.ok) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error(`Error fetching ${String(this._url)}: ${response.status} ${response.statusText}`);
}
let worker: ReadWorker;
let fileSize: number;
if (response.status === 206) {
fileSize = this._getTotalLengthFromRangeResponse(response);
worker = this._orchestrator.createWorker(0, Math.min(fileSize, URL_SOURCE_MIN_LOAD_AMOUNT));
} else {
// Server probably returned a 200.
const contentLength = response.headers.get('Content-Length');
if (contentLength) {
fileSize = Number(contentLength);
worker = this._orchestrator.createWorker(0, fileSize);
this._orchestrator.options.maxCacheSize = Infinity; // 🤷
console.warn(
'HTTP server did not respond with 206 Partial Content, meaning the entire remote resource now has'
+ ' to be downloaded. For efficient media file streaming across a network, please make sure your'
+ ' server supports range requests.',
);
} else {
throw new Error(`HTTP response (status ${response.status}) must surface Content-Length header.`);
}
}
this._orchestrator.fileSize = fileSize;
this._existingResponses.set(worker, { response, abortController });
this._orchestrator.runWorker(worker);
return fileSize;
_getFileSize(): number | null | undefined {
return this._fileSizeDetermined
? this._orchestrator.fileSize
: undefined;
}
/** @internal */
_read(start: number, end: number): MaybePromise<ReadResult> {
return this._orchestrator.read(start, end);
_read(
start: number,
end: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null> {
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
}
/** @internal */
private async _runWorker(worker: ReadWorker) {
// The outer loop is for resuming a request if it dies mid-response
while (true) {
const existing = this._existingResponses.get(worker);
this._existingResponses.delete(worker);
let abortController = existing?.abortController;
let response = existing?.response;
if (!abortController) {
abortController = new AbortController();
response = await retriedFetch(
const abortController = new AbortController();
const response = await retriedFetch(
this._options.fetchFn ?? fetch,
this._url,
mergeRequestInit(this._options.requestInit ?? {}, {
headers: {
// Always sending a range request is a good way to probe if the server supports them
Range: `bytes=${worker.currentPos}-`,
},
signal: abortController.signal,
@@ -534,20 +507,77 @@ export class UrlSource extends Source {
this._getRetryDelay,
() => this._disposed,
);
}
assert(response);
if (!response.ok) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error(`Error fetching ${String(this._url)}: ${response.status} ${response.statusText}`);
}
if (worker.currentPos > 0 && response.status !== 206) {
throw new Error(
'HTTP server did not respond with 206 Partial Content to a range request. To enable efficient media'
outer:
if (this._orchestrator.fileSize === null) {
// See if we can deduce the file size from the response
const contentRange = response.headers.get('Content-Range');
if (contentRange) {
const match = /\/(\d+)/.exec(contentRange);
if (match) {
this._orchestrator.supplyFileSize(Number(match[1]));
break outer;
}
}
const contentLength = response.headers.get('Content-Length');
if (contentLength) {
// Note: For range requests, this is _technically_ not correct, as the range response could contain
// less data than was requested. In practice, it seems most servers don't do this though, and the
// Content-Length header actually contains the length until the end of the file.
this._orchestrator.supplyFileSize(worker.currentPos + Number(contentLength));
}
}
this._fileSizeDetermined = true; // Yes, this is correct even if file size is still null
if (response.status !== 206) {
const origin = new URL(
this._url instanceof Request ? this._url.url : this._url,
typeof window !== 'undefined' ? window.location.href : undefined,
).origin;
if (origin !== 'null') {
if (!warnedOrigins.has(origin)) {
console.warn(
`HTTP server (origin ${origin}) did not respond to a range request with 206 Partial`
+ ' Content, meaning the entire resource will now be downloaded. To enable efficient media'
+ ' file streaming across a network, please make sure your server supports range requests.',
);
warnedOrigins.add(origin);
}
}
worker.currentPos = 0;
this._orchestrator.options.maxCacheSize = Infinity; // 🤷
if (this._orchestrator.fileSize !== null) {
worker.targetPos = this._orchestrator.fileSize;
} else {
// The server is dumb, doesn't even surface the content length, but we'll work with it.
worker.targetPos = Infinity;
worker.strictTarget = false;
}
// IN CASE there are other workers (rare), merge their pending slices into this worker and abort them
for (let i = 0; i < this._orchestrator.workers.length; i++) {
const otherWorker = this._orchestrator.workers[i]!;
if (otherWorker === worker) {
continue;
}
worker.pendingSlices.push(...otherWorker.pendingSlices);
otherWorker.aborted = true;
otherWorker.pendingSlices.length = 0;
this._orchestrator.workers.splice(i, 1);
i--;
}
}
if (!response.body) {
@@ -597,15 +627,20 @@ export class UrlSource extends Source {
if (done) {
if (worker.currentPos >= worker.targetPos) {
// All data was delivered, we're good
this._orchestrator.forgetWorker(worker);
worker.running = false;
this._orchestrator.onWorkerFinished(worker);
return;
}
if (worker.strictTarget) {
// The response stopped early, before the target. This can happen if server decides to cap range
// requests arbitrarily, even if the request had an uncapped end. In this case, let's fetch the rest
// of the data using a new request.
// requests arbitrarily, even if the request had an uncapped end. In this case, let's fetch the
// rest of the data using a new request.
break;
} else {
// Assume we have simply reached the end of the resource
this._orchestrator.onWorkerFinished(worker);
return;
}
}
this.onread?.(worker.currentPos, worker.currentPos + value.length);
@@ -708,13 +743,18 @@ export class FilePathSource extends Source {
}
/** @internal */
_read(start: number, end: number): MaybePromise<ReadResult> {
return this._streamSource._read(start, end);
_read(
start: number,
end: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null> {
return this._streamSource._read(start, end, minReadPosition, maxReadPosition);
}
/** @internal */
_retrieveSize(): MaybePromise<number> {
return this._streamSource._retrieveSize();
_getFileSize(): number | null | undefined {
return this._streamSource._getFileSize();
}
/** @internal */
@@ -815,7 +855,21 @@ export class StreamSource extends Source {
}
/** @internal */
_retrieveSize(): MaybePromise<number> {
_getFileSize(): number | null | undefined {
return this._orchestrator.fileSize ?? undefined;
}
/** @internal */
_read(
start: number,
end: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null> {
if (this._orchestrator.fileSize !== null) {
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
}
const result = this._options.getSize();
if (result instanceof Promise) {
@@ -825,7 +879,7 @@ export class StreamSource extends Source {
}
this._orchestrator.fileSize = size;
return size;
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
});
} else {
if (!Number.isInteger(result) || result < 0) {
@@ -833,15 +887,10 @@ export class StreamSource extends Source {
}
this._orchestrator.fileSize = result;
return result;
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
}
}
/** @internal */
_read(start: number, end: number): MaybePromise<ReadResult> {
return this._orchestrator.read(start, end);
}
/** @internal */
private async _runWorker(worker: ReadWorker) {
while (worker.currentPos < worker.targetPos && !worker.aborted) {
@@ -992,7 +1041,7 @@ export class ReadableStreamSource extends Source {
}
/** @internal */
_retrieveSize() {
_getFileSize(): number | null {
return this._endIndex; // Starts out as null, meaning this source is unsized
}
@@ -1251,7 +1300,7 @@ type PendingSlice = {
start: number;
end: number;
}[];
resolve: (bytes: Uint8Array) => void;
resolve: (bytes: Uint8Array | null) => void;
reject: (error: unknown) => void;
};
@@ -1267,6 +1316,8 @@ type ReadWorker = {
startPos: number;
currentPos: number;
targetPos: number;
/** The target is considered _strict_ when it is an error for the worker to terminate before reaching the target. */
strictTarget: boolean;
running: boolean;
aborted: boolean;
pendingSlices: PendingSlice[];
@@ -1296,19 +1347,18 @@ class ReadOrchestrator {
maxWorkerCount: number;
}) {}
read(innerStart: number, innerEnd: number): MaybePromise<ReadResult> {
assert(this.fileSize !== null);
read(
innerStart: number,
innerEnd: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null> {
const prefetchRange = this.options.prefetchProfile(innerStart, innerEnd, this.workers);
const outerStart = Math.max(prefetchRange.start, 0);
const outerEnd = Math.min(prefetchRange.end, this.fileSize);
const outerStart = Math.max(prefetchRange.start, minReadPosition);
const outerEnd = Math.min(prefetchRange.end, this.fileSize ?? Infinity, maxReadPosition);
assert(outerStart <= innerStart && innerEnd <= outerEnd);
let result: MaybePromise<{
bytes: Uint8Array;
view: DataView;
offset: number;
}> | null = null;
let result: MaybePromise<ReadResult | null> | null = null;
const innerCacheStartIndex = binarySearchLessOrEqual(this.cache, innerStart, x => x.start);
const innerStartEntry = innerCacheStartIndex !== -1 ? this.cache[innerCacheStartIndex] : null;
@@ -1400,7 +1450,7 @@ class ReadOrchestrator {
}
// We need to read more data, so now we're in async land
const { promise, resolve, reject } = promiseWithResolvers<Uint8Array>();
const { promise, resolve, reject } = promiseWithResolvers<Uint8Array | null>();
const innerHoles: typeof outerHoles = [];
for (const outerHole of outerHoles) {
@@ -1454,7 +1504,8 @@ class ReadOrchestrator {
if (!workerFound) {
// We need to spawn a new worker
const newWorker = this.createWorker(outerHole.start, outerHole.end);
const strictTarget = outerHole.end < outerEnd || this.fileSize !== null;
const newWorker = this.createWorker(outerHole.start, outerHole.end, strictTarget);
if (pendingSlice) {
newWorker.pendingSlices = [pendingSlice];
}
@@ -1465,11 +1516,12 @@ class ReadOrchestrator {
if (!result) {
assert(bytes);
result = promise.then(bytes => ({
result = promise.then(bytes => bytes && ({
bytes,
view: toDataView(bytes),
offset: innerStart,
}));
} satisfies ReadResult));
} else {
// The requested region was satisfied by the cache, but the entire prefetch region was not
}
@@ -1477,11 +1529,12 @@ class ReadOrchestrator {
return result;
}
createWorker(startPos: number, targetPos: number) {
createWorker(startPos: number, targetPos: number, strictTarget: boolean) {
const worker: ReadWorker = {
startPos,
currentPos: startPos,
targetPos,
strictTarget,
running: false,
// Due to async shenanigans, it can happen that workers are started after disposal. In this case, instead of
// simply not creating the worker, we allow it to run but immediately label it as aborted, so it can then
@@ -1608,11 +1661,48 @@ class ReadOrchestrator {
}
}
forgetWorker(worker: ReadWorker) {
supplyFileSize(size: number) {
assert(this.fileSize === null);
this.fileSize = size;
// Trim the workers with this new information
for (const worker of this.workers) {
worker.targetPos = Math.min(worker.targetPos, size);
worker.strictTarget = true;
for (let i = 0; i < worker.pendingSlices.length; i++) {
const pendingSlice = worker.pendingSlices[i]!;
for (const hole of pendingSlice.holes) {
if (hole.end > size) {
// Can't satisfy this slice anymore
pendingSlice.resolve(null);
worker.pendingSlices.splice(i, 1);
i--;
break;
}
}
}
}
}
/** Called when a worker reaches the end of the underlying data and must be cleaned up. */
onWorkerFinished(worker: ReadWorker) {
const index = this.workers.indexOf(worker);
assert(index !== -1);
this.workers.splice(index, 1);
if (this.fileSize === null) {
// We can now deduce the file size!
this.supplyFileSize(worker.currentPos);
}
for (const pendingSlice of worker.pendingSlices) {
pendingSlice.resolve(null);
}
}
insertIntoCache(entry: CacheEntry) {
@@ -1723,12 +1813,11 @@ class ReadOrchestrator {
* from another source.
*/
export class NullSource extends Source {
override _retrieveSize(): MaybePromise<number | null> {
override _getFileSize(): number | null {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override _read(start: number, end: number): MaybePromise<ReadResult | null> {
override _read(): MaybePromise<ReadResult | null> {
return null;
}
@@ -1753,21 +1842,41 @@ export class RangedSource extends Source {
this._length = length ?? null;
}
override async _retrieveSize(): Promise<number | null> {
const baseSize = await this._baseSource.getSizeOrNull(); // Call getSizeOrNull for memoization
override _getFileSize(): number | null | undefined {
const baseSize = this._baseSource._getFileSize();
if (baseSize === undefined) {
return this._length !== null
? this._length
: undefined;
}
if (baseSize === null) {
if (this._length !== null) {
return this._length;
} else {
return null;
}
}
return clamp(baseSize - this._offset, 0, this._length ?? Infinity);
}
override _read(start: number, end: number): MaybePromise<ReadResult | null> {
override _read(
start: number,
end: number,
minReadPosition: number,
maxReadPosition: number,
): MaybePromise<ReadResult | null> {
if (this._length !== null && end > this._length) {
return null;
}
const result = this._baseSource._read(this._offset + start, this._offset + end);
const result = this._baseSource._read(
this._offset + start,
this._offset + end,
this._offset + minReadPosition,
this._offset + maxReadPosition,
);
if (result instanceof Promise) {
return result.then((result) => {
+1 -1
View File
@@ -25,7 +25,7 @@ test('createAesDecryptStream', async () => {
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 reader = new Reader(source);
const stream = createAesDecryptStream(reader, () => ({ key, iv }));
const streamReader = stream.getReader();
+65
View File
@@ -810,3 +810,68 @@ test('MPEG-TS first packet key packet forcing', async () => {
expect(firstKeyPacketSeeked.sequenceNumber).toBe(firstPacket.sequenceNumber);
}
});
test('MPEG-TS without initial key packet', async () => {
for (let i = 0; i < 2; i++) {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/no-initial-keyframe.ts')),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
assert(videoTrack);
if (i === 1) {
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
}
const sink = new EncodedPacketSink(videoTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
expect(firstPacket.type).toBe('delta'); // First packet is delta
const noPacket = await sink.getKeyPacket(firstPacket.timestamp);
expect(noPacket).toBeNull();
const aKeyPacket = await sink.getKeyPacket(Infinity);
assert(aKeyPacket);
expect(aKeyPacket.type).toBe('key');
}
});
test('MPEG-TS with "extension" PES packets without PTS', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/entry24-segment3.ts')),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
assert(videoTrack);
const packetSizes: number[] = [];
const sink = new EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
packetSizes.push(packet.data.byteLength);
const seeked = await sink.getPacket(packet.timestamp);
expect(seeked!.timestamp).toBe(packet.timestamp);
expect(seeked!.sequenceNumber).toBe(packet.sequenceNumber);
}
expect(packetSizes).toEqual([
// These reference values come from ffprobe
30635, 5980, 3452, 2076, 2692, 9559, 5754, 2171, 2989, 13962,
4156, 209260, 25698, 152, 2299, 128, 94, 102, 74, 28898,
233, 127, 99, 1449, 130, 99, 3988, 202, 130, 139,
92, 18652, 200, 146, 98, 8666, 190, 151, 156, 108,
9991, 162, 97, 81, 9929, 130, 106, 13362, 133, 117,
7606, 274, 109, 106, 12359, 218, 123, 111, 24109, 261,
142, 120, 13417, 789, 153, 139, 12490, 549, 192, 135,
13191, 428, 159, 150, 30815, 309, 135, 119, 9231, 354,
147, 116, 11835, 263, 162, 103, 6693, 202, 100, 101,
4693, 223, 144, 174, 118, 9155, 1188, 379, 169, 213,
]);
});
Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
#EXTM3U
## Archived from https://player.webvideocore.net/CL1olYogIrDWvwqiIKK7eLBkzvO18gwo9ERMzsyXzwt_t-ya8ygf2kQBZww38JJT/8i4vvznv8408.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=759977, RESOLUTION=1280x720
mp3-in-ts-level/index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=759977, RESOLUTION=1280x720
mp3-in-ts-level/index.m3u8
+109
View File
@@ -0,0 +1,109 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
media_w905519204_0.mp3
#EXTINF:10.0,
media_w905519204_1.mp3
#EXTINF:10.0,
media_w905519204_2.mp3
#EXTINF:10.0,
media_w905519204_3.mp3
#EXTINF:10.0,
media_w905519204_4.mp3
#EXTINF:10.0,
media_w905519204_5.mp3
#EXTINF:10.0,
media_w905519204_6.mp3
#EXTINF:10.0,
media_w905519204_7.mp3
#EXTINF:10.0,
media_w905519204_8.mp3
#EXTINF:10.0,
media_w905519204_9.mp3
#EXTINF:10.0,
media_w905519204_10.mp3
#EXTINF:10.0,
media_w905519204_11.mp3
#EXTINF:10.0,
media_w905519204_12.mp3
#EXTINF:10.0,
media_w905519204_13.mp3
#EXTINF:10.0,
media_w905519204_14.mp3
#EXTINF:10.0,
media_w905519204_15.mp3
#EXTINF:10.0,
media_w905519204_16.mp3
#EXTINF:10.0,
media_w905519204_17.mp3
#EXTINF:10.0,
media_w905519204_18.mp3
#EXTINF:10.0,
media_w905519204_19.mp3
#EXTINF:10.0,
media_w905519204_20.mp3
#EXTINF:10.0,
media_w905519204_21.mp3
#EXTINF:10.0,
media_w905519204_22.mp3
#EXTINF:10.0,
media_w905519204_23.mp3
#EXTINF:10.0,
media_w905519204_24.mp3
#EXTINF:10.0,
media_w905519204_25.mp3
#EXTINF:10.0,
media_w905519204_26.mp3
#EXTINF:10.0,
media_w905519204_27.mp3
#EXTINF:10.0,
media_w905519204_28.mp3
#EXTINF:10.0,
media_w905519204_29.mp3
#EXTINF:10.0,
media_w905519204_30.mp3
#EXTINF:10.0,
media_w905519204_31.mp3
#EXTINF:10.0,
media_w905519204_32.mp3
#EXTINF:10.0,
media_w905519204_33.mp3
#EXTINF:10.0,
media_w905519204_34.mp3
#EXTINF:10.0,
media_w905519204_35.mp3
#EXTINF:10.0,
media_w905519204_36.mp3
#EXTINF:10.0,
media_w905519204_37.mp3
#EXTINF:10.0,
media_w905519204_38.mp3
#EXTINF:10.0,
media_w905519204_39.mp3
#EXTINF:10.0,
media_w905519204_40.mp3
#EXTINF:10.0,
media_w905519204_41.mp3
#EXTINF:10.0,
media_w905519204_42.mp3
#EXTINF:10.0,
media_w905519204_43.mp3
#EXTINF:10.0,
media_w905519204_44.mp3
#EXTINF:10.0,
media_w905519204_45.mp3
#EXTINF:10.0,
media_w905519204_46.mp3
#EXTINF:10.0,
media_w905519204_47.mp3
#EXTINF:10.0,
media_w905519204_48.mp3
#EXTINF:10.0,
media_w905519204_49.mp3
#EXTINF:10.0,
media_w905519204_50.mp3
#EXTINF:6.833,
media_w905519204_51.mp3
#EXT-X-ENDLIST
+4
View File
@@ -0,0 +1,4 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=63701,CODECS="mp4a.40.34"
chunklist_w905519204.m3u8
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version v2.3.0-5bf8ad5-release
#EXT-X-TARGETDURATION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="v-0576p-1400k-libx264-init.mp4"
#EXTINF:4.000,
v-0576p-1400k-libx264-s1.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s2.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s3.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s4.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s5.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s6.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s7.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s8.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s9.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s10.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s11.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s12.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s13.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s14.mp4
#EXTINF:4.000,
v-0576p-1400k-libx264-s15.mp4
#EXT-X-ENDLIST
+25
View File
@@ -0,0 +1,25 @@
#EXTM3U
## Generated with https://github.com/google/shaka-packager version v2.3.0-5bf8ad5-release
#EXT-X-MEDIA:TYPE=AUDIO,URI="playlist_a-eng-0128k-aac-2c.mp4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="en",NAME="stream_5",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="playlist_a-deu-0128k-aac-2c.mp4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="de",NAME="stream_4",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="playlist_a-ita-0128k-aac-2c.mp4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="it",NAME="stream_8",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="playlist_a-fra-0128k-aac-2c.mp4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="fr",NAME="stream_7",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="playlist_a-spa-0128k-aac-2c.mp4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="es",NAME="stream_9",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="playlist_a-eng-0384k-aac-6c.mp4.m3u8",GROUP-ID="default-audio-group",LANGUAGE="en",NAME="stream_6",CHANNELS="6"
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="playlist_s-en.webvtt.m3u8",GROUP-ID="default-text-group",LANGUAGE="en",NAME="stream_0",DEFAULT=YES,AUTOSELECT=YES
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="playlist_s-el.webvtt.m3u8",GROUP-ID="default-text-group",LANGUAGE="el",NAME="stream_1",AUTOSELECT=YES
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="playlist_s-fr.webvtt.m3u8",GROUP-ID="default-text-group",LANGUAGE="fr",NAME="stream_2",AUTOSELECT=YES
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="playlist_s-pt-BR.webvtt.m3u8",GROUP-ID="default-text-group",LANGUAGE="pt-BR",NAME="stream_3",AUTOSELECT=YES
#EXT-X-STREAM-INF:BANDWIDTH=8062646,AVERAGE-BANDWIDTH=1824731,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=768x576,AUDIO="default-audio-group",SUBTITLES="default-text-group"
playlist_v-0576p-1400k-libx264.mp4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=6096050,AVERAGE-BANDWIDTH=1410993,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=640x480,AUDIO="default-audio-group",SUBTITLES="default-text-group"
playlist_v-0480p-1000k-libx264.mp4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4005148,AVERAGE-BANDWIDTH=1146764,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=480x360,AUDIO="default-audio-group",SUBTITLES="default-text-group"
playlist_v-0360p-0750k-libx264.mp4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2190444,AVERAGE-BANDWIDTH=784019,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=320x240,AUDIO="default-audio-group",SUBTITLES="default-text-group"
playlist_v-0240p-0400k-libx264.mp4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=827972,AVERAGE-BANDWIDTH=482170,CODECS="avc1.42c01e,mp4a.40.2",RESOLUTION=192x144,AUDIO="default-audio-group",SUBTITLES="default-text-group"
playlist_v-0144p-0100k-libx264.mp4.m3u8
Binary file not shown.
+87
View File
@@ -0,0 +1,87 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:5
#EXTINF:4.313,
media-u5ce0h2ki_0.ts
#EXTINF:4.800,
media-u5ce0h2ki_1.ts
#EXTINF:2.400,
media-u5ce0h2ki_2.ts
#EXTINF:4.800,
media-u5ce0h2ki_3.ts
#EXTINF:4.800,
media-u5ce0h2ki_4.ts
#EXTINF:2.400,
media-u5ce0h2ki_5.ts
#EXTINF:4.800,
media-u5ce0h2ki_6.ts
#EXTINF:4.800,
media-u5ce0h2ki_7.ts
#EXTINF:2.400,
media-u5ce0h2ki_8.ts
#EXTINF:4.800,
media-u5ce0h2ki_9.ts
#EXTINF:4.800,
media-u5ce0h2ki_10.ts
#EXTINF:2.400,
media-u5ce0h2ki_11.ts
#EXTINF:4.800,
media-u5ce0h2ki_12.ts
#EXTINF:4.800,
media-u5ce0h2ki_13.ts
#EXTINF:2.400,
media-u5ce0h2ki_14.ts
#EXTINF:4.800,
media-u5ce0h2ki_15.ts
#EXTINF:4.800,
media-u5ce0h2ki_16.ts
#EXTINF:2.400,
media-u5ce0h2ki_17.ts
#EXTINF:4.800,
media-u5ce0h2ki_18.ts
#EXTINF:4.800,
media-u5ce0h2ki_19.ts
#EXTINF:2.400,
media-u5ce0h2ki_20.ts
#EXTINF:4.800,
media-u5ce0h2ki_21.ts
#EXTINF:4.800,
media-u5ce0h2ki_22.ts
#EXTINF:2.400,
media-u5ce0h2ki_23.ts
#EXTINF:4.800,
media-u5ce0h2ki_24.ts
#EXTINF:4.800,
media-u5ce0h2ki_25.ts
#EXTINF:2.400,
media-u5ce0h2ki_26.ts
#EXTINF:4.800,
media-u5ce0h2ki_27.ts
#EXTINF:4.800,
media-u5ce0h2ki_28.ts
#EXTINF:2.400,
media-u5ce0h2ki_29.ts
#EXTINF:4.800,
media-u5ce0h2ki_30.ts
#EXTINF:4.800,
media-u5ce0h2ki_31.ts
#EXTINF:2.400,
media-u5ce0h2ki_32.ts
#EXTINF:4.800,
media-u5ce0h2ki_33.ts
#EXTINF:4.800,
media-u5ce0h2ki_34.ts
#EXTINF:2.400,
media-u5ce0h2ki_35.ts
#EXTINF:4.800,
media-u5ce0h2ki_36.ts
#EXTINF:4.800,
media-u5ce0h2ki_37.ts
#EXTINF:2.400,
media-u5ce0h2ki_38.ts
#EXTINF:4.800,
media-u5ce0h2ki_39.ts
#EXTINF:4.800,
media-u5ce0h2ki_40.ts
#EXT-X-ENDLIST
+18
View File
@@ -0,0 +1,18 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=678000,CODECS="avc1.4d0029,mp4a.40.2",RESOLUTION=768x432
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=678000,CODECS="avc1.4d0029,mp4a.40.2",RESOLUTION=768x432
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=2320000,CODECS="avc1.4d0029,mp4a.40.2",RESOLUTION=1280x720
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=2320000,CODECS="avc1.4d0029,mp4a.40.2",RESOLUTION=1280x720
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=198000,CODECS="avc1.42e020,mp4a.40.2",RESOLUTION=480x270
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=198000,CODECS="avc1.42e020,mp4a.40.2",RESOLUTION=480x270
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1756000,CODECS="avc1.4d0029,mp4a.40.2",RESOLUTION=864x486
[email protected]
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1756000,CODECS="avc1.4d0029,mp4a.40.2",RESOLUTION=864x486
[email protected]
Binary file not shown.
Binary file not shown.
+66
View File
@@ -0,0 +1,66 @@
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:NO
#EXT-X-VERSION:2
#EXT-X-TARGETDURATION:8
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag1Num0.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag1Num1.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag2Num2.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag3Num3.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag4Num4.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag5Num5.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag6Num6.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag7Num7.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag8Num8.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag9Num9.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag10Num10.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag11Num11.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag12Num12.ts
#EXTINF:8,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag13Num13.ts
#EXTINF:3,
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-06-17T18:32:00Z
fms_sampleFrag14Num14.ts
#EXT-X-ENDLIST
+36
View File
@@ -0,0 +1,36 @@
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXT-X-VERSION:3
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:10:03+00:00
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:10:03+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:11:06+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:23:23+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:23:23+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:23:29+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:24:56+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2012-12-06T19:25:03+00:00
#EXTINF:6.006,{}
fragment.ts
#EXT-X-ENDLIST
+44
View File
@@ -0,0 +1,44 @@
#EXTM3U
#EXT-X-VERSION:1
## Created with Unified Streaming Platform(version=1.6.6)
#EXT-X-MEDIA-SEQUENCE:176505193
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:10
#EXT-X-PROGRAM-DATE-TIME:TEST1
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505193.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505194.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505195.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505196.ts
#EXTINF:10, no desc
interpretation-audio=64000-video=500000-176505197.ts
#EXT-X-PROGRAM-DATE-TIME:TEST3
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505198.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505199.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505200.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505201.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505202.ts
#EXTINF:10, no desc
interpretation-audio=64000-video=500000-176505203.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505204.ts
#EXT-X-PROGRAM-DATE-TIME:TEST4
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505205.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505206.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505207.ts
#EXTINF:8, no desc
interpretation-audio=64000-video=500000-176505208.ts
#EXTINF:10, no desc
interpretation-audio=64000-video=500000-176505209.ts
#EXT-X-ENDLIST
+5
View File
@@ -0,0 +1,5 @@
#EXTM3U
#EXT-X-VERSION:1
## Created with Unified Streaming Platform(version=1.6.6)
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=597840,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=640x360
interpretation-audio=64000-video=500000.m3u8
+17
View File
@@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-VERSION:1
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:136803485
#EXT-X-KEY:METHOD=NONE
#EXT-X-PROGRAM-DATE-TIME:2013-05-08T17:40:50Z
#EXTINF:10,
segment0.ts
#EXTINF:10,
segment1.ts
#EXTINF:10,
segment2.ts
#EXTINF:10,
segment3.ts
#EXTINF:10,
segment4.ts
#EXT-X-ENDLIST
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:14
#EXT-X-MEDIA-SEQUENCE:37121
#EXTINF:10.224,
media_w506387179_37121.ts
#EXTINF:10.239,
media_w506387179_37122.ts
#EXTINF:10.223,
media_w506387179_37123.ts
#EXT-X-ENDLIST
+5
View File
@@ -0,0 +1,5 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=281439,CODECS="avc1.66.31,mp4a.40.2",RESOLUTION=640x360
popularteutonia-video/popularteutonia-video/chunklist_w506387179.m3u8
+16
View File
@@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Chinese",FORCED=NO,AUTOSELECT=YES,URI="chinese/ed.m3u8",LANGUAGE="zh"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="French",FORCED=NO,AUTOSELECT=YES,URI="french/ed.m3u8",LANGUAGE="fr"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,URI="media/b160000-english.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="sp",NAME="Spanish",DEFAULT=NO,AUTOSELECT=YES,URI="media/b160000-spanish.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="Commentary (eng)",DEFAULT=NO,AUTOSELECT=NO,URI="media/b160000-commentary.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2962000,NAME="Main",CODECS="avc1.66.30",RESOLUTION=1280x720,AUDIO="aac",SUBTITLES="subs"
media/b2962000-video.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1427000,NAME="Main",CODECS="avc1.66.30",RESOLUTION=768x432,AUDIO="aac",SUBTITLES="subs"
media/b1427000-video.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=688000,NAME="Main",CODECS="avc1.66.30",RESOLUTION=448x252,AUDIO="aac",SUBTITLES="subs"
media/b688000-video.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=331000,NAME="Main",CODECS="avc1.66.30",RESOLUTION=284x160,AUDIO="aac",SUBTITLES="subs"
media/b331000-video.m3u8
+7
View File
@@ -0,0 +1,7 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="stereo",LANGUAGE="en",NAME="English"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="stereo",LANGUAGE="dubbing",NAME="Dubbing",URI="audio/stereo/none/128kbit.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=831270,CODECS="mp4a.40.2",AUDIO="stereo"
audio/stereo/en/128kbit.m3u8
+236
View File
@@ -0,0 +1,236 @@
#EXTM3U
#EXT-X-TARGETDURATION:8
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="main.mp4",BYTERANGE="1118@0"
#EXTINF:7.98333,
#EXT-X-BYTERANGE:1700094@1118
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1789481@1701212
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1777588@3490693
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1752144@5268281
main.mp4
#EXTINF:7.26667,
#EXT-X-BYTERANGE:1563219@7020425
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1801953@8583644
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1834406@10385597
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1785475@12220003
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1823288@14005478
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1829143@15828766
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1800453@17657909
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1803327@19458362
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1873636@21261689
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1948640@23135325
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1840513@25083965
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1937752@26924478
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1997584@28862230
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1924169@30859814
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1895068@32783983
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1934194@34679051
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2038511@36613245
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1930106@38651756
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1922710@40581862
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2055801@42504572
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1963459@44560373
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1960831@46523832
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2038186@48484663
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2065137@50522849
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1960086@52587986
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1984801@54548072
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2061777@56532873
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2006124@58594650
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2025156@60600774
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2043210@62625930
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2043844@64669140
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1993528@66712984
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1998870@68706512
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2099623@70705382
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2162266@72805005
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:1997458@74967271
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2075347@76964729
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2180556@79040076
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2023098@81220632
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2053500@83243730
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2074093@85297230
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2133059@87371323
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2015841@89504382
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2051188@91520223
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2149171@93571411
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2103043@95720582
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2039241@97823625
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2127240@99862866
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2162908@101990106
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2067454@104153014
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2038579@106220468
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2119116@108259047
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2142043@110378163
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2049034@112520206
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2163788@114569240
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2171266@116733028
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2136193@118904294
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2113276@121040487
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2205504@123153763
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2237552@125359267
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2129080@127596819
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2115078@129725899
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2258589@131840977
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2177462@134099566
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2174962@136277028
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2253878@138451990
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2361884@140705868
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2272546@143067752
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2228068@145340298
main.mp4
#EXTINF:8.00000,
#EXT-X-BYTERANGE:2320491@147568366
main.mp4
#EXTINF:7.96667,
#EXT-X-BYTERANGE:2087892@149888857
main.mp4
#EXTINF:0.78333,
#EXT-X-BYTERANGE:195177@151976749
main.mp4
#EXT-X-ENDLIST
+152
View File
@@ -0,0 +1,152 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="a1",NAME="English",LANGUAGE="en-US",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="a1/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="a2",NAME="English",LANGUAGE="en-US",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="6",URI="a2/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="a3",NAME="English",LANGUAGE="en-US",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="6",URI="a3/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,INSTREAM-ID="CC1"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,FORCED=NO,URI="s1/en/prog_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=928091,BANDWIDTH=1015727,CODECS="avc1.640028",RESOLUTION=1920x1080,URI="tp5/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=731514,BANDWIDTH=760174,CODECS="avc1.64001f",RESOLUTION=1280x720,URI="tp4/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=509153,BANDWIDTH=520162,CODECS="avc1.64001f",RESOLUTION=960x540,URI="tp3/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=176942,BANDWIDTH=186651,CODECS="avc1.64001f",RESOLUTION=640x360,URI="tp2/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=90796,BANDWIDTH=95410,CODECS="avc1.64001f",RESOLUTION=480x270,URI="tp1/iframe_index.m3u8"
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2190673,BANDWIDTH=2523597,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v5/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=8052613,BANDWIDTH=9873268,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v9/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6133114,BANDWIDTH=7318337,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v8/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4681537,BANDWIDTH=5421720,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v7/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3183969,BANDWIDTH=3611257,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v6/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1277747,BANDWIDTH=1475903,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v4/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=890848,BANDWIDTH=1017705,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v3/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=533420,BANDWIDTH=582820,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v2/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=303898,BANDWIDTH=339404,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=416x234,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v1/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2413172,BANDWIDTH=2746096,CODECS="avc1.640020,ac-3",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v5/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=8275112,BANDWIDTH=10095767,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v9/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6355613,BANDWIDTH=7540836,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v8/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4904036,BANDWIDTH=5644219,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v7/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3406468,BANDWIDTH=3833756,CODECS="avc1.640020,ac-3",RESOLUTION=1280x720,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v6/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1500246,BANDWIDTH=1698402,CODECS="avc1.64001f,ac-3",RESOLUTION=768x432,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v4/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1113347,BANDWIDTH=1240204,CODECS="avc1.64001f,ac-3",RESOLUTION=640x360,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v3/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=755919,BANDWIDTH=805319,CODECS="avc1.64001f,ac-3",RESOLUTION=480x270,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v2/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=526397,BANDWIDTH=561903,CODECS="avc1.64001f,ac-3",RESOLUTION=416x234,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v1/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2221172,BANDWIDTH=2554096,CODECS="avc1.640020,ec-3",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v5/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=8083112,BANDWIDTH=9903767,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v9/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6163613,BANDWIDTH=7348836,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v8/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4712036,BANDWIDTH=5452219,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v7/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3214468,BANDWIDTH=3641756,CODECS="avc1.640020,ec-3",RESOLUTION=1280x720,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v6/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1308246,BANDWIDTH=1506402,CODECS="avc1.64001f,ec-3",RESOLUTION=768x432,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v4/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=921347,BANDWIDTH=1048204,CODECS="avc1.64001f,ec-3",RESOLUTION=640x360,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v3/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=563919,BANDWIDTH=613319,CODECS="avc1.64001f,ec-3",RESOLUTION=480x270,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v2/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=334397,BANDWIDTH=369903,CODECS="avc1.64001f,ec-3",RESOLUTION=416x234,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v1/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=287207,BANDWIDTH=328352,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,URI="tp10/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=216605,BANDWIDTH=226274,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,URI="tp9/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=154000,BANDWIDTH=159037,CODECS="hvc1.2.4.L123.B0",RESOLUTION=960x540,URI="tp8/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=90882,BANDWIDTH=92800,CODECS="hvc1.2.4.L123.B0",RESOLUTION=640x360,URI="tp7/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=50569,BANDWIDTH=51760,CODECS="hvc1.2.4.L123.B0",RESOLUTION=480x270,URI="tp6/iframe_index.m3u8"
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1966314,BANDWIDTH=2164328,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v14/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6105163,BANDWIDTH=6664228,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v18/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4801073,BANDWIDTH=5427899,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v17/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3441312,BANDWIDTH=4079770,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v16/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2635933,BANDWIDTH=2764701,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v15/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1138612,BANDWIDTH=1226255,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v13/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=829339,BANDWIDTH=901770,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v12/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=522229,BANDWIDTH=548927,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v11/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=314941,BANDWIDTH=340713,CODECS="hvc1.2.4.L123.B0,mp4a.40.2",RESOLUTION=416x234,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a1",SUBTITLES="sub1"
v10/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2188813,BANDWIDTH=2386827,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v14/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6327662,BANDWIDTH=6886727,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v18/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=5023572,BANDWIDTH=5650398,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v17/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3663811,BANDWIDTH=4302269,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v16/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2858432,BANDWIDTH=2987200,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=1280x720,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v15/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1361111,BANDWIDTH=1448754,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=768x432,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v13/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1051838,BANDWIDTH=1124269,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=640x360,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v12/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=744728,BANDWIDTH=771426,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=480x270,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v11/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=537440,BANDWIDTH=563212,CODECS="hvc1.2.4.L123.B0,ac-3",RESOLUTION=416x234,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a2",SUBTITLES="sub1"
v10/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1996813,BANDWIDTH=2194827,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v14/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6135662,BANDWIDTH=6694727,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v18/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4831572,BANDWIDTH=5458398,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v17/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3471811,BANDWIDTH=4110269,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v16/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2666432,BANDWIDTH=2795200,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=1280x720,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v15/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1169111,BANDWIDTH=1256754,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=768x432,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v13/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=859838,BANDWIDTH=932269,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=640x360,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v12/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=552728,BANDWIDTH=579426,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=480x270,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v11/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=345440,BANDWIDTH=371212,CODECS="hvc1.2.4.L123.B0,ec-3",RESOLUTION=416x234,FRAME-RATE=30.000,CLOSED-CAPTIONS="cc",AUDIO="a3",SUBTITLES="sub1"
v10/prog_index.m3u8
+50
View File
@@ -0,0 +1,50 @@
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:2
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=2.171
#EXT-X-PART-INF:PART-TARGET=1.034
#EXT-X-MAP:URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/18446744073709551615.m4s?skid=default&signature=NjlhMDE5YTBfZTkxMzAwNTZiOTczOTI3NTgwOThlNDU4NzA0ZTc5NjlkMmI4MjYyZWVjN2Q5Yjk0OTVlZTQxNTRlZTM0NzU3OA==&zone=0"
#EXT-X-MEDIA-SEQUENCE:13767
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:04:50.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13767.m4s?skid=default&signature=NjlhMDE5YTBfNjk5YzgwNTRlMDg3NGZkYjdjMzYzMWUzMDc1ZjdlMmFmOGVhNzY5NDk4MmFhOTEwNWQ3ZjBlMDM2ZjIyY2RmNA==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:04:52.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13768.m4s?skid=default&signature=NjlhMDE5YTBfZmIzOTE1OTYwNTdkZjFhYzA0ZDgxNjZkMjI4N2QyZmM4ZDQ5YjQyN2QwMzIzNGE5MmZjODJkNDM4ZjA4YWRlZA==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:04:54.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13769.m4s?skid=default&signature=NjlhMDE5YTBfNTU1ZDNlODkzZTYwYTcxZGM5ZjJlMzJhMGQ5YWI4MTMzZGI0YzdiYjRmZjQ5MTFkYzM1YWE4ZTFkYWQzM2NmNQ==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:04:56.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13770.m4s?skid=default&signature=NjlhMDE5YTBfZTQxYTZjYWMzMzc5ZjcxZWUwZmY0M2FmNWQ4ZDcxNjVjOGJmYTY0M2RjOTdiMDExMDhjNTQ4M2QzMDA0ZWQ0Yw==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:04:58.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13771.m4s?skid=default&signature=NjlhMDE5YTBfODdkYmZiYmI3NDZjMzdmNmNkMDhjNjQzMjA5YjQ3YTI3NzYwNmVlY2JmNjYxZDEzMGM2ZWY4ZWZkMWExODdhNw==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:05:00.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13772.m4s?skid=default&signature=NjlhMDE5YTBfOTFjOTU5YWE4NzJlZmYyOTkyNzI5NjhiNDdiMzBhYmEwZjYzZjU1ZTFlYzM4MjhkZDIwZjhhZWFjMzRkOGVmYg==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:05:02.593+00:00
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13773.m4s?skid=default&signature=NjlhMDE5YTBfMTQ2NzA2NjY2OGFlNzUyZjZiZTllNDhjMWMzZjQxMDcwNWMxY2U0MWU5NzFjZDgwM2VlMzlmMjRmY2U0ZTMxYg==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:05:04.593+00:00
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13774.0.m4s?skid=default&signature=NjlhMDE5YTBfMTFhZmNjNGJlODk1NzM3NGQ1NmU2ZmEyMzU0OTAwNTdkMDVhOWE4Yjg5ZDMyYmViZmNhNjU1MmI1ZjNlNjhlMQ==&zone=0",INDEPENDENT=YES
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13774.1.m4s?skid=default&signature=NjlhMDE5YTBfMzNjMzc0ZjgwNGQ1MTI5MzE3OTc1YjNmM2FjNzE2Y2U5ZDU3YjIzNWIyMDQ2NzAxNTVkNzRjMGMwMmE0YmVlOQ==&zone=0"
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13774.m4s?skid=default&signature=NjlhMDE5YTBfMzZlNDBhYjQ0YTlmNGNiODZkNTRiYmYwMzk4ZGUxYzVmN2M5Mzk2YTA3MTA2MDQ1NmJkNzM3Mzk3YTA0YjVlNA==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:05:06.593+00:00
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13775.0.m4s?skid=default&signature=NjlhMDE5YTBfMjNjMGVjMzdkYzA5MjlmOWU2Y2JjMTQ0NjdiYmFmYTgyMTcwYzU1ZDM5ZmJhMjMwNzFjNGExNmM4NTU4Y2ZjZg==&zone=0",INDEPENDENT=YES
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13775.1.m4s?skid=default&signature=NjlhMDE5YTBfZTkyYWI2MDA4NjAyNjI3OGM2NmM2YjNmOTdhMzcwOWNlMTEwZmU0ZmM1OGYzN2E1YmFkZTQzYTQwMWQyYTRiMg==&zone=0"
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13775.m4s?skid=default&signature=NjlhMDE5YTBfZTI1NjVjZTgzNzNjOGFkYjdiODEzNzY5N2ZiNWY3NjZlYTg0M2ViZDc3ZDdhZTExMjk5MjI4ZTI5YjllNGVlZA==&zone=0
#EXT-X-PROGRAM-DATE-TIME:2026-02-19T10:05:08.593+00:00
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13776.0.m4s?skid=default&signature=NjlhMDE5YTBfMjEwNjMyZGVlYzY4MmZlNDA2ZDhlNmM2Yzg0ZTg3YThlMDkyMDE1YzhmNTdlMDg1MjU0NWM3ZDdlMmI1OTNlOA==&zone=0",INDEPENDENT=YES
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13776.1.m4s?skid=default&signature=NjlhMDE5YTBfODQzYzFhZThlMzViMGUwY2ZmNzAzMDViODA5NmMwZDkyMDc2MzBmOGI4YzJmY2U4ZjgwYzQ3ZDRkNTkxYzMyOQ==&zone=0"
#EXTINF:2,
https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13776.m4s?skid=default&signature=NjlhMDE5YTBfODFjZDNlMjEyOTRiNzg5ODJhNzYyN2Y4YTA0MWM0NjFkOTliMWZlNjUwNGI0ZWIwMjc5YjRiNzk3ODdjMzgxMg==&zone=0
#EXT-X-PART:DURATION=1,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13777.0.m4s?skid=default&signature=NjlhMDE5YTBfYmNiOWJiZjIxMGUwZWI2MDM3ZGRiMzFmNTFiZGY0MWZiYjQ2NDAzZmZmZWM4N2E2MmNiOWJmNzMzNTQ4Yzk3Yw==&zone=0",INDEPENDENT=YES
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="https://chunk-gcp-us-east1-vop1.fastly.mux.com/v1/chunk/qgC2PzuWbZKlPMYq76Ou02pdqXx00ohqZa7q9iXse2UvJdBXkgAkWF1jOw2GA02W7dFr8NA01xeI7NsC9nFYL8cbM1jngZy39vkNmyH7azaHbXM/13777.1.m4s?skid=default&signature=NjlhMDE5YTBfZWE1ZjlkNzQ0NzFmYWI4YzViZmFjNjFmZjJlYmI1MGQ2OWVhNzgzOTUyYjIyNGJkY2FhNDEzNGFkZGU0MTA1NA==&zone=0"
#EXT-X-RENDITION-REPORT:URI="/exHWYv6RMb5Ebv02Qg1jgY8nm45rh4g02M7J4rgKQOTrAW2j5eO1a9p7yeI7r252brzExe4NqZMhacJ9WvVLbQPXEMjj3pOB6cQH01v7TmrgEg/rendition.m3u8?cdn=fastly&expires=1772100000&live=1&skid=default&signature=NjlhMDE5YTBfYTcyNDI4NDIwZjlkNGQ4NTJjZjhhNmMxYzczZWViNzE1ZmM4NzhmNDMxNTU0ZTE5MzVlZDJkZDgwYmYyMTg5NA=="
#EXT-X-RENDITION-REPORT:URI="/402gWK4kVgmdBJzX1Pc00DEFywkLj0139RYuIZtepGttudbFdMzlikw7E1FGY5vNhmuAkV01xkRItrBcIQTygNRYbwxYHRZ00feMU/rendition.m3u8?cdn=fastly&expires=1772100000&live=1&skid=default&signature=NjlhMDE5YTBfNjgzODA5NTJmODY1MDkxZmU4MTgwNmZhZjM4MTgwMDUxMzliMzc5OWY4YzlhMDBiN2MwYWZmNjlkM2M3NTY0MQ=="
#EXT-X-RENDITION-REPORT:URI="/5uM3MOkfoRnOqNUfk1JVRfFfBcvoVhtOnJxF01fIhPRxviPL6ToqgB38Rqm3IjIXzWs00HpC4i9I5z022NcRgVVieDmvqlBWj00mBZClXyn84Dw/rendition.m3u8?cdn=fastly&expires=1772100000&live=1&skid=default&signature=NjlhMDE5YTBfMDY2M2Q1YzdlZGM4ODM1MGUxYzU0NTkxNGNmMDE0MWUxZTNlNzg4M2ZiM2YxNWQ2ZDM0ODQ5OTlkNDE2MTdiZQ=="
#EXT-X-RENDITION-REPORT:URI="/dfjhdoc1n4ZvHXGPL4mX01srgXbDUDV9sGPnyUvCfiy5jObUqQxwWCBM6n78osvlRPIONlmcD00Fea100au6lg6NrsFZcoOL9K2/rendition.m3u8?cdn=fastly&expires=1772100000&live=1&skid=default&signature=NjlhMDE5YTBfNDc1NDk0MzIzNzJhOTgzYjJhYzQ3ZWFlYzcwNWIxZjM1M2I1NjZhNWFiNGMwNjUyYmY3NDE2MTQ5YjkzMjY3OA=="
#EXT-X-RENDITION-REPORT:URI="/H01ZlTPGBC3UShrFGCoIvq4ZiYpIYCGKXZ0002Pw02cpsnKaQFDWALC7gQsyRXe95E84px2THVcoa7bRW4iMNCWjhg/rendition.m3u8?cdn=fastly&expires=1772100000&live=1&skid=default&signature=NjlhMDE5YTBfNWRmMDg5MDhkMjc4MDdhZDhiNzI3ZmU1ZmFiMzIzMTc1MDVhOWZiMTRkNTMxMDliMWM2YTBjMzQ5OWYwNTVlZg=="
+15
View File
@@ -0,0 +1,15 @@
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi-0",NAME="Default",CHANNELS="2",AUTOSELECT=YES,DEFAULT=YES,URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/x02cwe8AnQdIrmeTtwxjV4CwAYmGv2Af0002ni01fu5201ZcMDOmmqlkm2XxrYBuq02qrACvqwi202NxAYgGYDrnkng1w/rendition.m3u8?cdn=fastly&expires=1772121600&live=1&skid=edgemv-default-1&signature=NjlhMDZlMDBfOWQ1NjY2NzFjOTg0Mjc1OTc4NTM5OTc5NWQ3YTFhNzM1NWNhZDJmOGE1NmJlNDUwYmRmNTg4YWUyNjAyODZkOA==&vsid=KkV1Lvb8OsQe6uONjPJcYxRSKkd7cev9LnmtlvUqEypaZpls4wIPLRzIAroJnP0000XtIWjzEq00oY"
#EXT-X-STREAM-INF:BANDWIDTH=2564100,AVERAGE-BANDWIDTH=2564100,CODECS="mp4a.40.2,avc1.640020",AUDIO="audio-hi-0",RESOLUTION=1280x720,CLOSED-CAPTIONS=NONE
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/Ajw5FEXHZ2kfb009Eynt01PGs01C014n6DOsbZu5bzX6mNzSWkx3uNURbdCEhvg1OlpjYxY2QIAO012mD1E007JaL029sdohSJ00JBzLwJ2bTvd4Kcw/rendition.m3u8?cdn=fastly&expires=1772121600&live=1&skid=edgemv-default-1&signature=NjlhMDZlMDBfMGIyMGUyMjIwM2ZhZTk3NjM1Yzc2NWFmYWI3Y2M1YWVmNzYyNzNlZWU5ZGE5ODRlMjZiMTZlZDJkY2Q4MDhhNQ==&vsid=KkV1Lvb8OsQe6uONjPJcYxRSKkd7cev9LnmtlvUqEypaZpls4wIPLRzIAroJnP0000XtIWjzEq00oY
#EXT-X-STREAM-INF:BANDWIDTH=1696200,AVERAGE-BANDWIDTH=1696200,CODECS="mp4a.40.2,avc1.640020",AUDIO="audio-hi-0",RESOLUTION=960x540,CLOSED-CAPTIONS=NONE
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/lfHuw00BszIttr55HYSItzrAuO5lCI1lXnlNd43Zf5rirXQIkMCfezeR01J01GH75aazTAI4kTQAgP2hRB500iXHnbYCOvsBlPtuJ5KCJ00Xg6u00/rendition.m3u8?cdn=fastly&expires=1772121600&live=1&skid=edgemv-default-1&signature=NjlhMDZlMDBfMmQ0ZDIwMjkxYmUxZGQ5ZjMyMDljOWFjZDJlZTIyZTU5ODA4MmU3NTEyZTc1NTZiMjNhZGIyNDgyZWM3N2RmNg==&vsid=KkV1Lvb8OsQe6uONjPJcYxRSKkd7cev9LnmtlvUqEypaZpls4wIPLRzIAroJnP0000XtIWjzEq00oY
#EXT-X-STREAM-INF:BANDWIDTH=4712400,AVERAGE-BANDWIDTH=4712400,CODECS="mp4a.40.2,avc1.64002a",AUDIO="audio-hi-0",RESOLUTION=1920x1080,CLOSED-CAPTIONS=NONE
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/xIb3vQ2JY02bdd9noWFI8CA4EEA38ttChhpg1601Vi8KPra1lHJVR1bRNKsqUFmjns8ICU00FQCdcQhY02wFYVlk00W6FZXni5PNC/rendition.m3u8?cdn=fastly&expires=1772121600&live=1&skid=edgemv-default-1&signature=NjlhMDZlMDBfMDVjODEyZGY1NjcxZjhmN2ZiYmU1MjE1ZTJlNjI4N2IwNmIxMDdmMGQ1ZDRlMTU5ZjQ2MWRlZjRkODQ2MWNhNw==&vsid=KkV1Lvb8OsQe6uONjPJcYxRSKkd7cev9LnmtlvUqEypaZpls4wIPLRzIAroJnP0000XtIWjzEq00oY
#EXT-X-STREAM-INF:BANDWIDTH=987800,AVERAGE-BANDWIDTH=987800,CODECS="mp4a.40.2,avc1.64001f",AUDIO="audio-hi-0",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/OAh8j7wI02wIZjlj11ITDTbFv02iLRViF024rFITqf00zna4mDKj2L268Se5rlkLdXC83RvWMsVLn9KkpIDy0055ZdGzfU005CfyXN7Q85QO00N2ug/rendition.m3u8?cdn=fastly&expires=1772121600&live=1&skid=edgemv-default-1&signature=NjlhMDZlMDBfNTA4MWQ2NWQzMTc0NDU4OGUwNGQ2ZjdkNmQzNTIxZTU3NjcyZTg1M2VkMDFlNDU3OTA0M2E1ZDBjMzliZjdlMQ==&vsid=KkV1Lvb8OsQe6uONjPJcYxRSKkd7cev9LnmtlvUqEypaZpls4wIPLRzIAroJnP0000XtIWjzEq00oY
#EXT-X-STREAM-INF:BANDWIDTH=700700,AVERAGE-BANDWIDTH=700700,CODECS="mp4a.40.2,avc1.64001e",AUDIO="audio-hi-0",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/YnwKeA9pBuwssrpLK6AMhUEbtDrlsLeBf2JVqgAhkQqcCWxP9sd1OOq01fcJEXelm85KMt01EdDhOlxdPZKHgUeubMY00T02FZ5L/rendition.m3u8?cdn=fastly&expires=1772121600&live=1&skid=edgemv-default-1&signature=NjlhMDZlMDBfYjVhNTNhYWEyOThiNWQ3YTBjNzY2MmUzOGVlYjMyOGNiZGFlMWEwYjEzMmFmMDQ5ZWU3ODBjYjkzZWFiOTA3YQ==&vsid=KkV1Lvb8OsQe6uONjPJcYxRSKkd7cev9LnmtlvUqEypaZpls4wIPLRzIAroJnP0000XtIWjzEq00oY
+17
View File
@@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="text-540/playlist.m3u8",GROUP-ID="text-540",NAME="Text"
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="text-720/playlist.m3u8",GROUP-ID="text-720",NAME="Text"
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="text-1080/playlist.m3u8",GROUP-ID="text-1080",NAME="Text"
#EXT-X-MEDIA:TYPE=AUDIO,URI="audio-540/playlist.m3u8",GROUP-ID="audio-540",LANGUAGE="en",NAME="ENGLISH",CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="audio-720/playlist.m3u8",GROUP-ID="audio-720",LANGUAGE="en",NAME="ENGLISH",CHANNELS="2"
#EXT-X-MEDIA:TYPE=AUDIO,URI="audio-1080/playlist.m3u8",GROUP-ID="audio-1080",LANGUAGE="en",NAME="ENGLISH",CHANNELS="2"
#EXT-X-STREAM-INF:BANDWIDTH=240648,AVERAGE-BANDWIDTH=229254,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=960x540,AUDIO="audio-540",SUBTITLES="text-540"
video-540/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=273583,AVERAGE-BANDWIDTH=261082,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720,AUDIO="audio-720",SUBTITLES="text-720"
video-720/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=446911,AVERAGE-BANDWIDTH=416641,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,AUDIO="audio-1080",SUBTITLES="text-1080"
video-1080/playlist.m3u8
https://mtoczko.github.io/hls-test-streams/test-group/video-540/playlist.m3u8
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+113
View File
@@ -0,0 +1,113 @@
#EXTM3U
#EXT-X-VERSION:5
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:4
#EXTINF:4.0,
video/720p/ts/segment_0.ts
#EXTINF:4.0,
video/720p/ts/segment_1.ts
#EXTINF:4.0,
video/720p/ts/segment_2.ts
#EXTINF:4.0,
video/720p/ts/segment_3.ts
#EXTINF:4.0,
video/720p/ts/segment_4.ts
#EXTINF:4.0,
video/720p/ts/segment_5.ts
#EXTINF:4.0,
video/720p/ts/segment_6.ts
#EXTINF:4.0,
video/720p/ts/segment_7.ts
#EXTINF:4.0,
video/720p/ts/segment_8.ts
#EXTINF:4.0,
video/720p/ts/segment_9.ts
#EXTINF:4.0,
video/720p/ts/segment_10.ts
#EXTINF:4.0,
video/720p/ts/segment_11.ts
#EXTINF:4.0,
video/720p/ts/segment_12.ts
#EXTINF:4.0,
video/720p/ts/segment_13.ts
#EXTINF:4.0,
video/720p/ts/segment_14.ts
#EXTINF:4.0,
video/720p/ts/segment_15.ts
#EXTINF:4.0,
video/720p/ts/segment_16.ts
#EXTINF:4.0,
video/720p/ts/segment_17.ts
#EXTINF:4.0,
video/720p/ts/segment_18.ts
#EXTINF:4.0,
video/720p/ts/segment_19.ts
#EXTINF:4.0,
video/720p/ts/segment_20.ts
#EXTINF:4.0,
video/720p/ts/segment_21.ts
#EXTINF:4.0,
video/720p/ts/segment_22.ts
#EXTINF:4.0,
video/720p/ts/segment_23.ts
#EXTINF:4.0,
video/720p/ts/segment_24.ts
#EXTINF:4.0,
video/720p/ts/segment_25.ts
#EXTINF:4.0,
video/720p/ts/segment_26.ts
#EXTINF:4.0,
video/720p/ts/segment_27.ts
#EXTINF:4.0,
video/720p/ts/segment_28.ts
#EXTINF:4.0,
video/720p/ts/segment_29.ts
#EXTINF:4.0,
video/720p/ts/segment_30.ts
#EXTINF:4.0,
video/720p/ts/segment_31.ts
#EXTINF:4.0,
video/720p/ts/segment_32.ts
#EXTINF:4.0,
video/720p/ts/segment_33.ts
#EXTINF:4.0,
video/720p/ts/segment_34.ts
#EXTINF:4.0,
video/720p/ts/segment_35.ts
#EXTINF:4.0,
video/720p/ts/segment_36.ts
#EXTINF:4.0,
video/720p/ts/segment_37.ts
#EXTINF:4.0,
video/720p/ts/segment_38.ts
#EXTINF:4.0,
video/720p/ts/segment_39.ts
#EXTINF:4.0,
video/720p/ts/segment_40.ts
#EXTINF:4.0,
video/720p/ts/segment_41.ts
#EXTINF:4.0,
video/720p/ts/segment_42.ts
#EXTINF:4.0,
video/720p/ts/segment_43.ts
#EXTINF:4.0,
video/720p/ts/segment_44.ts
#EXTINF:4.0,
video/720p/ts/segment_45.ts
#EXTINF:4.0,
video/720p/ts/segment_46.ts
#EXTINF:4.0,
video/720p/ts/segment_47.ts
#EXTINF:4.0,
video/720p/ts/segment_48.ts
#EXTINF:4.0,
video/720p/ts/segment_49.ts
#EXTINF:4.0,
video/720p/ts/segment_50.ts
#EXTINF:4.0,
video/720p/ts/segment_51.ts
#EXTINF:2.280000000000001,
video/720p/ts/segment_52.ts
#EXT-X-ENDLIST
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
+4 -2
View File
@@ -6,7 +6,9 @@ Also, why not just have the packet metadata on the packet? I think that would ma
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
- EXT-X-MEDIA (DONE, but surface more metadata from it, like language and shit) - should it be passed down to the tracks??
- getSegments() API? Any point in partially reading the playlist file? Idk
dont forget ext x media + stream inf both situation
- Add warnings when piping m3u8 or dash shit into Input
reachorchestrator worker count is not really correct, or at least, urlsource parallelism is not really correct I think.