mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 16:53:47 +02:00
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:
+40
-1
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;">
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
+585
-475
File diff suppressed because it is too large
Load Diff
+25
-27
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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=="
|
||||
@@ -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
|
||||
@@ -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.
@@ -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.
@@ -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.
@@ -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.
|
||||
Reference in New Issue
Block a user