mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
[WIP] Add majority of logic for HLS reading support
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Reader } from '../../src/reader.js';
|
||||
import { BufferSource } from '../../src/source.js';
|
||||
import { createAesDecryptStream } from '../../src/aes.js';
|
||||
|
||||
// getRandomValues is length-limited, so let's just do this
|
||||
export const fillRandom = <T extends Uint8Array>(buffer: T) => {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
test('createAesDecryptStream', async () => {
|
||||
// Test for all paddings
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const plaintextLength = Math.floor(Math.random() * 2 ** 18) + i;
|
||||
|
||||
const plaintext = fillRandom(new Uint8Array(plaintextLength));
|
||||
const key = fillRandom(new Uint8Array(16));
|
||||
const iv = fillRandom(new Uint8Array(16));
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']);
|
||||
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext));
|
||||
|
||||
const source = new BufferSource(ciphertext);
|
||||
const reader = await Reader.fromSource(source);
|
||||
|
||||
const stream = createAesDecryptStream(reader, () => ({ key, iv }));
|
||||
const streamReader = stream.getReader();
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await streamReader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
// Concatenate chunks
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const decrypted = new Uint8Array(totalLength);
|
||||
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
decrypted.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
expect(decrypted.length).toBe(plaintext.length);
|
||||
|
||||
// .toEqual is slow, so we do this instead
|
||||
for (let j = 0; j < plaintext.length; j++) {
|
||||
if (decrypted[j] !== plaintext[j]) {
|
||||
throw new Error(`Mismatch at byte ${j} for padding ${16 - (i % 16)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { joinPaths } from '../../src/misc.js';
|
||||
|
||||
test('joinPaths handles all path combinations correctly', () => {
|
||||
// Simple relative paths
|
||||
expect(joinPaths('path/to/entry.m3u8', 'other.m3u8')).toBe('path/to/other.m3u8');
|
||||
expect(joinPaths('entry.m3u8', 'other.m3u8')).toBe('other.m3u8');
|
||||
expect(joinPaths('/path/to/entry.m3u8', 'other.m3u8')).toBe('/path/to/other.m3u8');
|
||||
|
||||
// Absolute relative paths (starting with /)
|
||||
expect(joinPaths('path/to/entry.m3u8', '/other.m3u8')).toBe('/other.m3u8');
|
||||
expect(joinPaths('/path/to/entry.m3u8', '/other.m3u8')).toBe('/other.m3u8');
|
||||
|
||||
// With protocols
|
||||
expect(joinPaths('https://example.com/path/to/entry.m3u8', 'other.m3u8'))
|
||||
.toBe('https://example.com/path/to/other.m3u8');
|
||||
expect(joinPaths('https://example.com/path/to/entry.m3u8', '/other.m3u8'))
|
||||
.toBe('https://example.com/other.m3u8');
|
||||
expect(joinPaths('file:///path/to/entry.m3u8', 'other.m3u8')).toBe('file:///path/to/other.m3u8');
|
||||
expect(joinPaths('file:///path/to/entry.m3u8', '/other.m3u8')).toBe('file:///other.m3u8');
|
||||
|
||||
// With ./
|
||||
expect(joinPaths('path/to/entry.m3u8', './other.m3u8')).toBe('path/to/other.m3u8');
|
||||
expect(joinPaths('https://example.com/path/to/entry.m3u8', './other.m3u8'))
|
||||
.toBe('https://example.com/path/to/other.m3u8');
|
||||
|
||||
// With ../
|
||||
expect(joinPaths('path/to/entry.m3u8', '../other.m3u8')).toBe('path/other.m3u8');
|
||||
expect(joinPaths('path/to/deep/entry.m3u8', '../../other.m3u8')).toBe('path/other.m3u8');
|
||||
expect(joinPaths('https://example.com/path/to/entry.m3u8', '../other.m3u8'))
|
||||
.toBe('https://example.com/path/other.m3u8');
|
||||
expect(joinPaths('https://example.com/a/b/c/entry.m3u8', '../../other.m3u8'))
|
||||
.toBe('https://example.com/a/other.m3u8');
|
||||
|
||||
// Mixed ./ and ../
|
||||
expect(joinPaths('path/to/entry.m3u8', './../other.m3u8')).toBe('path/other.m3u8');
|
||||
expect(joinPaths('path/to/entry.m3u8', '../foo/../other.m3u8')).toBe('path/other.m3u8');
|
||||
|
||||
// Second argument is a full URL with protocol
|
||||
expect(joinPaths('path/to/entry.m3u8', 'https://example.com/other.m3u8'))
|
||||
.toBe('https://example.com/other.m3u8');
|
||||
expect(joinPaths('https://example.com/path/to/entry.m3u8', 'https://other.com/other.m3u8'))
|
||||
.toBe('https://other.com/other.m3u8');
|
||||
expect(joinPaths('file:///path/to/entry.m3u8', 'https://example.com/other.m3u8'))
|
||||
.toBe('https://example.com/other.m3u8');
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { test } from 'vitest';
|
||||
import { ManifestInput } from '../../src/manifest-input.js';
|
||||
import { UrlSource } from '../../src/source.js';
|
||||
import { ALL_MANIFEST_FORMATS } from '../../src/manifest-input-format.js';
|
||||
import { ALL_FORMATS, MPEG_TS } from '../../src/input-format.js';
|
||||
import { EncodedPacketSink } from '../../src/media-sink.js';
|
||||
import { assert } from '../../src/misc.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
|
||||
test('yo', { timeout: 60_000 }, async () => {
|
||||
/*
|
||||
const yo = new Input({
|
||||
source: new UrlSource('https://test-streams.mux.dev/x36xhzz/url_0/url_462/193039199_mp4_h264_aac_hd_7.ts'),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = await yo.getPrimaryVideoTrack();
|
||||
const sink1 = new EncodedPacketSink(videoTrack!);
|
||||
|
||||
console.log(sink1.getFirstPacket());
|
||||
|
||||
for await (const packet of sink1.packets()) {
|
||||
console.log(performance.now(), packet.timestamp);
|
||||
}
|
||||
|
||||
console.log('Don');
|
||||
|
||||
return;
|
||||
|
||||
*/
|
||||
const manifest = new ManifestInput({
|
||||
entryPath: 'https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8'
|
||||
?? 'https://playertest.longtailvideo.com/adaptive/customIV/prog_index.m3u8'
|
||||
?? 'https://playertest.longtailvideo.com/adaptive/issue666/playlists/cisq0gim60007xzvi505emlxx.m3u8'
|
||||
?? 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8'
|
||||
?? 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8'
|
||||
?? 'https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8'
|
||||
?? 'https://cdn.jwplayer.com/manifests/pZxWPRg4.m3u8'
|
||||
?? 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'
|
||||
?? 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8',
|
||||
getSource: path => new UrlSource(path),
|
||||
manifestFormats: ALL_MANIFEST_FORMATS,
|
||||
mediaFormats: [MPEG_TS],
|
||||
});
|
||||
|
||||
const variants = await manifest.getVariants();
|
||||
const variant = variants[1]!;
|
||||
const segment = await variant.getFirstSegment();
|
||||
const thing = await segment!.toInput();
|
||||
console.log(await thing.getTracks());
|
||||
/*
|
||||
|
||||
const primaryVariant = (await manifest.getPrimaryVariant())!;
|
||||
const segment = (await primaryVariant.getFirstSegment())!;
|
||||
const thing = await segment.toInput();
|
||||
console.log(await thing.getTracks());
|
||||
*/
|
||||
|
||||
return;
|
||||
|
||||
for await (const segment of primaryVariant.segments()) {
|
||||
console.log(segment.relativeTimestamp, segment.initSegment?.relativeTimestamp);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
const input = await manifest.toInput();
|
||||
const track = await input.getPrimaryVideoTrack();
|
||||
const sink = new EncodedPacketSink(track!);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet.timestamp);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
// const primaryVariant = (await manifest.getPrimaryVariant())!;
|
||||
|
||||
for await (const segment of primaryVariant.segments()) {
|
||||
console.log(segment.location);
|
||||
}
|
||||
|
||||
/*
|
||||
for await (const segment of primaryVariant.segments()) {
|
||||
const input = await segment.toInput();
|
||||
console.log(segment.path, segment.discontinuity, await input.getFirstTimestamp());
|
||||
continue;
|
||||
|
||||
if (segment.path.endsWith('u-6400-m-720x408-1628-a-96-1-1.ts')) {
|
||||
const input = await segment.toInput();
|
||||
const track = (await input.getPrimaryVideoTrack())!;
|
||||
const timestamp = await track.getFirstTimestamp();
|
||||
console.log(timestamp, await track.getDecoderConfig());
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const segment = await primaryVariant.getFirstSegment();
|
||||
|
||||
// console.log(segment);
|
||||
|
||||
const input = primaryVariant.toInput();
|
||||
// console.log(input);
|
||||
|
||||
// const tracks = await input.getTracks();
|
||||
const track = await input.getPrimaryVideoTrack();
|
||||
|
||||
const sink = new EncodedPacketSink(track!);
|
||||
let currentPacket = await sink.getFirstPacket();
|
||||
|
||||
while (currentPacket) {
|
||||
console.log(currentPacket.timestamp);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
*/
|
||||
});
|
||||
Reference in New Issue
Block a user