mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-04 22:33:49 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a099b4259 | ||
|
|
172b4a1fd2 | ||
|
|
43c1ea7efd | ||
|
|
cab92c55ec | ||
|
|
81bc9d7c44 | ||
|
|
3209363470 | ||
|
|
d16b1927ad | ||
|
|
f620a328e5 | ||
|
|
7aa0fa465e | ||
|
|
cc5f7d8e8f | ||
|
|
7e1c93c331 | ||
|
|
5b88332cb8 | ||
|
|
9a70ccbacd | ||
|
|
8c67872e83 | ||
|
|
7f4093fd43 | ||
|
|
b63db955f6 | ||
|
|
92454b28f7 | ||
|
|
db3cb143fe | ||
|
|
e20f779ede | ||
|
|
e1e32c9505 | ||
|
|
79f19b6717 | ||
|
|
98387258a4 |
@@ -1,4 +1,6 @@
|
||||
- Prefer functions declared using const, not using the function keyword
|
||||
- Code style is tab indent with semicolons
|
||||
- Mediabunny core code is contained in src/, extensions are in packages/*/, website is in docs/
|
||||
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general catergory of thing that is being tested, not after any individual single test.
|
||||
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general catergory of thing that is being tested, not after any individual single test.
|
||||
- Avoid ifs without a {} block. So no if (cond) return;, always do if (cond) { return; }
|
||||
- `type` instead of `interface` for object types
|
||||
@@ -51,6 +51,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
### Bronze sponsors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://elevenlabs.io/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/elevenlabs.png" width="40" height="40" alt="ElevenLabs">
|
||||
</a>
|
||||
|
||||
<a href="https://www.reactvideoeditor.com/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/rve.png" width="40" height="40" alt="React Video Editor">
|
||||
</a>
|
||||
|
||||
@@ -306,8 +306,8 @@ const conversion = await Conversion.init({
|
||||
output,
|
||||
|
||||
// Function gets invoked for each video track:
|
||||
video: (videoTrack, n) => {
|
||||
if (n > 1) {
|
||||
video: (videoTrack) => {
|
||||
if (videoTrack.number > 1) {
|
||||
// Keep only the first video track
|
||||
return { discard: true };
|
||||
}
|
||||
@@ -319,7 +319,7 @@ const conversion = await Conversion.init({
|
||||
},
|
||||
|
||||
// Async functions work too:
|
||||
audio: async (audioTrack, n) => {
|
||||
audio: async (audioTrack) => {
|
||||
if (audioTrack.languageCode !== 'rus') {
|
||||
// Keep only Russian audio tracks
|
||||
return { discard: true };
|
||||
|
||||
@@ -556,14 +556,16 @@ const output = new Output(...);
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
video: track => ({
|
||||
width: 480,
|
||||
bitrate: QUALITY_LOW,
|
||||
},
|
||||
audio: {
|
||||
discard: track.number > 1, // Keep only the first video track
|
||||
}),
|
||||
audio: track => ({
|
||||
numberOfChannels: 1,
|
||||
bitrate: QUALITY_LOW,
|
||||
},
|
||||
discard: track.number > 1, // Keep only the first audio track
|
||||
}),
|
||||
trim: {
|
||||
// Let's keep only the first 60 seconds
|
||||
start: 0,
|
||||
|
||||
@@ -98,6 +98,10 @@ Once you have an `InputTrack`, you can start extracting metadata from it.
|
||||
// Get a unique ID for this track in the input file:
|
||||
track.id; // => number
|
||||
|
||||
// Get the 1-based index of this track among all tracks of the same type
|
||||
// (e.g., first video track is 1, second video track is 2, etc.):
|
||||
track.number; // => number
|
||||
|
||||
// Check the track's type:
|
||||
track.type; // => 'video' | 'audio' | 'subtitle';
|
||||
|
||||
@@ -502,6 +506,9 @@ type UrlSourceOptions = {
|
||||
// in memory. Defaults to 8 MiB.
|
||||
maxCacheSize?: number;
|
||||
|
||||
// The maximum number of parallel requests to use for fetching. Defaults to 2.
|
||||
parallelism?: number;
|
||||
|
||||
// Used to provide a custom fetch function
|
||||
fetchFn?: typeof fetch;
|
||||
};
|
||||
|
||||
@@ -101,6 +101,7 @@ const sponsors = {
|
||||
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
|
||||
],
|
||||
bronze: [
|
||||
{ image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' },
|
||||
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
|
||||
{ image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' },
|
||||
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
|
||||
@@ -112,6 +113,7 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/4714175', name: 'Phoomparin Mano', url: 'https://github.com/heypoom' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||
@@ -120,6 +122,8 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/56988069', name: 'SyhabouthAlex', url: 'https://github.com/SyhabouthAlex' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/38181164', name: 'wcw', url: 'https://github.com/asd55667' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/1836701', name: 'Bean Deng', url: 'https://github.com/HADB' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/255616819', name: 'cronischarles-del', url: 'https://github.com/cronischarles-del' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/37973863', name: 'Gomi', url: 'https://github.com/gxy5202' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -62,13 +62,15 @@ const compressFile = async (resource: File | string) => {
|
||||
currentConversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
video: track => ({
|
||||
width: 320, // Height will be deduced automatically to retain aspect ratio
|
||||
bitrate: QUALITY_VERY_LOW,
|
||||
},
|
||||
audio: {
|
||||
discard: track.number > 1, // Keep only the first video track
|
||||
}),
|
||||
audio: track => ({
|
||||
bitrate: 32e3,
|
||||
},
|
||||
discard: track.number > 1, // Keep only the first audio track
|
||||
}),
|
||||
});
|
||||
|
||||
// Keep track of progress
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.29.0",
|
||||
"version": "1.32.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.29.0",
|
||||
"version": "1.32.0",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7739,9 +7739,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.28.0.tgz",
|
||||
"integrity": "sha512-D63nzvBRIBSUsRgaIfFugWCy2iOV5T/C6nHn2fW0aWqyRuSGzWsVMXzlNi3iCKieoA/WECYJg8oVGtUukpy3XQ==",
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.31.1.tgz",
|
||||
"integrity": "sha512-7ijrySW675UTxXoAji8Xj/H8H1uyZhPDARlNO1FF1zGnzJV3snayU32c1bpumslx5vbUa/M7fm6kcG/3y7UxHQ==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12065,7 +12065,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.29.0",
|
||||
"version": "1.32.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.29.0",
|
||||
"version": "1.32.0",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.29.0",
|
||||
"version": "1.32.0",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -12,6 +12,17 @@ import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared
|
||||
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
|
||||
import createWorker from './encode.worker';
|
||||
|
||||
const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
'[WARNING]\n@mediabunny/mp3-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,'
|
||||
+ ' or if something is being bundled incorrectly.',
|
||||
);
|
||||
}
|
||||
(globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL] = true;
|
||||
|
||||
class Mp3Encoder extends CustomAudioEncoder {
|
||||
private worker: Worker | null = null;
|
||||
private nextMessageId = 0;
|
||||
|
||||
@@ -8,9 +8,15 @@
|
||||
|
||||
import { aacChannelMap, aacFrequencyTable, AudioCodec } from '../codec';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import {
|
||||
ID3_V2_HEADER_SIZE,
|
||||
parseId3V2Tag,
|
||||
readId3V2Header,
|
||||
} from '../id3';
|
||||
import { Input } from '../input';
|
||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import {
|
||||
assert,
|
||||
AsyncMutex,
|
||||
@@ -20,7 +26,6 @@ import {
|
||||
} from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { readBytes, Reader } from '../reader';
|
||||
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
|
||||
import {
|
||||
AdtsFrameHeader,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
@@ -43,6 +48,7 @@ export class AdtsDemuxer extends Demuxer {
|
||||
metadataPromise: Promise<void> | null = null;
|
||||
firstFrameHeader: AdtsFrameHeader | null = null;
|
||||
loadedSamples: Sample[] = [];
|
||||
metadataTags: MetadataTags | null = null;
|
||||
|
||||
tracks: InputAudioTrack[] = [];
|
||||
|
||||
@@ -73,6 +79,26 @@ export class AdtsDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async advanceReader() {
|
||||
if (this.lastLoadedPos === 0) {
|
||||
// Skip all ID3v2 tags at the start of the file
|
||||
while (true) {
|
||||
let slice = this.reader.requestSlice(this.lastLoadedPos, ID3_V2_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
|
||||
if (!slice) {
|
||||
this.lastSampleLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.lastLoadedPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
}
|
||||
|
||||
let slice = this.reader.requestSliceRange(
|
||||
this.lastLoadedPos,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
@@ -135,7 +161,41 @@ export class AdtsDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async getMetadataTags() {
|
||||
return {}; // No tags in this one
|
||||
const release = await this.readingMutex.acquire();
|
||||
|
||||
try {
|
||||
await this.readMetadata();
|
||||
|
||||
if (this.metadataTags) {
|
||||
return this.metadataTags;
|
||||
}
|
||||
|
||||
this.metadataTags = {};
|
||||
let currentPos = 0;
|
||||
|
||||
while (true) {
|
||||
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||
if (!headerSlice) break;
|
||||
|
||||
const id3V2Header = readId3V2Header(headerSlice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
|
||||
if (contentSlice instanceof Promise) contentSlice = await contentSlice;
|
||||
if (!contentSlice) break;
|
||||
|
||||
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
|
||||
|
||||
currentPos = headerSlice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
return this.metadataTags;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +206,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import { parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
|
||||
import { Id3V2Writer } from '../id3';
|
||||
import { metadataTagsAreEmpty } from '../metadata';
|
||||
import { assert, Bitstream, toUint8Array } from '../misc';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack } from '../output';
|
||||
@@ -30,7 +32,10 @@ export class AdtsMuxer extends Muxer {
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Nothing needed here
|
||||
if (!metadataTagsAreEmpty(this.output._metadataTags)) {
|
||||
const id3Writer = new Id3V2Writer(this.writer);
|
||||
id3Writer.writeId3V2Tag(this.output._metadataTags);
|
||||
}
|
||||
}
|
||||
|
||||
async getMimeType() {
|
||||
|
||||
+2
-10
@@ -161,10 +161,6 @@ export const iterateAvcNalUnits = (packetData: Uint8Array, decoderConfig: VideoD
|
||||
}
|
||||
};
|
||||
|
||||
export const iterateAvcNalUnitsAnnexB = function* (packetData: Uint8Array): Generator<NalUnitLocation> {
|
||||
yield* iterateNalUnitsInAnnexB(packetData);
|
||||
};
|
||||
|
||||
export const extractNalUnitTypeForAvc = (byte: number) => {
|
||||
return byte & 0x1F;
|
||||
};
|
||||
@@ -275,7 +271,7 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array): Av
|
||||
const ppsUnits: Uint8Array[] = [];
|
||||
const spsExtUnits: Uint8Array[] = [];
|
||||
|
||||
for (const loc of iterateAvcNalUnitsAnnexB(packetData)) {
|
||||
for (const loc of iterateNalUnitsInAnnexB(packetData)) {
|
||||
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
|
||||
const type = extractNalUnitTypeForAvc(nalUnit[0]!);
|
||||
|
||||
@@ -849,10 +845,6 @@ export const iterateHevcNalUnits = (packetData: Uint8Array, decoderConfig: Video
|
||||
}
|
||||
};
|
||||
|
||||
export const iterateHevcNalUnitsAnnexB = function* (packetData: Uint8Array): Generator<NalUnitLocation> {
|
||||
yield* iterateNalUnitsInAnnexB(packetData);
|
||||
};
|
||||
|
||||
export const extractNalUnitTypeForHevc = (byte: number) => {
|
||||
return (byte >> 1) & 0x3F;
|
||||
};
|
||||
@@ -1014,7 +1006,7 @@ export const extractHevcDecoderConfigurationRecord = (packetData: Uint8Array) =>
|
||||
const ppsUnits: Uint8Array[] = [];
|
||||
const seiUnits: Uint8Array[] = [];
|
||||
|
||||
for (const loc of iterateHevcNalUnitsAnnexB(packetData)) {
|
||||
for (const loc of iterateNalUnitsInAnnexB(packetData)) {
|
||||
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
|
||||
const type = extractNalUnitTypeForHevc(nalUnit[0]!);
|
||||
|
||||
|
||||
+9
-3
@@ -66,7 +66,8 @@ export type ConversionOptions = {
|
||||
* Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a
|
||||
* function, it will be invoked for each video track and is expected to return or resolve to the options
|
||||
* for that specific track. The function is passed an instance of {@link InputVideoTrack} as well as a number `n`,
|
||||
* which is the 1-based index of the track in the list of all video tracks.
|
||||
* which is the 1-based index of the track in the list of all video tracks. Using `n` is deprecated, prefer the
|
||||
* identical `track.number` instead.
|
||||
*/
|
||||
video?: ConversionVideoOptions
|
||||
| ((track: InputVideoTrack, n: number) => MaybePromise<ConversionVideoOptions | undefined>);
|
||||
@@ -75,7 +76,8 @@ export type ConversionOptions = {
|
||||
* Audio-specific options. When passing an object, the same options are applied to all audio tracks. When passing a
|
||||
* function, it will be invoked for each audio track and is expected to return or resolve to the options
|
||||
* for that specific track. The function is passed an instance of {@link InputAudioTrack} as well as a number `n`,
|
||||
* which is the 1-based index of the track in the list of all audio tracks.
|
||||
* which is the 1-based index of the track in the list of all audio tracks. Using `n` is deprecated, prefer the
|
||||
* identical `track.number` instead.
|
||||
*/
|
||||
audio?: ConversionAudioOptions
|
||||
| ((track: InputAudioTrack, n: number) => MaybePromise<ConversionAudioOptions | undefined>);
|
||||
@@ -799,9 +801,13 @@ export class Conversion {
|
||||
this._executed = true;
|
||||
|
||||
if (this.onProgress) {
|
||||
// Compute duration using only the utilized tracks
|
||||
const durationPromises = this.utilizedTracks.map(x => x.computeDuration());
|
||||
const duration = Math.max(0, ...await Promise.all(durationPromises));
|
||||
|
||||
this._computeProgress = true;
|
||||
this._totalDuration = Math.min(
|
||||
(await this.input.computeDuration()) - this._startTimestamp,
|
||||
duration - this._startTimestamp,
|
||||
this._endTimestamp - this._startTimestamp,
|
||||
);
|
||||
|
||||
|
||||
@@ -529,6 +529,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getCodec() {
|
||||
return 'flac' as const;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,17 @@
|
||||
/// <reference types="dom-mediacapture-transform" preserve="true" />
|
||||
/// <reference types="dom-webcodecs" preserve="true" />
|
||||
|
||||
const MEDIABUNNY_LOADED_SYMBOL = Symbol.for('mediabunny loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
'[WARNING]\nMediabunny was loaded twice.'
|
||||
+ ' This will likely cause Mediabunny not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of Mediabunny,'
|
||||
+ ' or if something is being bundled incorrectly.',
|
||||
);
|
||||
}
|
||||
(globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL] = true;
|
||||
|
||||
export {
|
||||
Output,
|
||||
OutputOptions,
|
||||
|
||||
+33
-13
@@ -21,14 +21,14 @@ import {
|
||||
} from './matroska/ebml';
|
||||
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
|
||||
import { Mp3Demuxer } from './mp3/mp3-demuxer';
|
||||
import { FRAME_HEADER_SIZE } from '../shared/mp3-misc';
|
||||
import { FRAME_HEADER_SIZE, getXingOffset, INFO, XING } from '../shared/mp3-misc';
|
||||
import { ID3_V2_HEADER_SIZE, readId3V2Header } from './id3';
|
||||
import { readNextMp3FrameHeader } from './mp3/mp3-reader';
|
||||
import { OggDemuxer } from './ogg/ogg-demuxer';
|
||||
import { WaveDemuxer } from './wave/wave-demuxer';
|
||||
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts/adts-reader';
|
||||
import { AdtsDemuxer } from './adts/adts-demuxer';
|
||||
import { readAscii, readBytes } from './reader';
|
||||
import { readAscii, readBytes, readU32Be } from './reader';
|
||||
import { FlacDemuxer } from './flac/flac-demuxer';
|
||||
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer';
|
||||
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc';
|
||||
@@ -261,12 +261,7 @@ export class WebMInputFormat extends MatroskaInputFormat {
|
||||
export class Mp3InputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
let slice = input._reader.requestSlice(0, 10);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
let currentPos = 0;
|
||||
let id3V2HeaderFound = false;
|
||||
|
||||
while (true) {
|
||||
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
@@ -278,7 +273,6 @@ export class Mp3InputFormat extends InputFormat {
|
||||
break;
|
||||
}
|
||||
|
||||
id3V2HeaderFound = true;
|
||||
currentPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
@@ -287,8 +281,18 @@ export class Mp3InputFormat extends InputFormat {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id3V2HeaderFound) {
|
||||
// If there was an ID3v2 tag at the start, we can be pretty sure this is MP3 by now
|
||||
const firstHeader = firstResult.header;
|
||||
const xingOffset = getXingOffset(firstHeader.mpegVersionId, firstHeader.channel);
|
||||
|
||||
let slice = input._reader.requestSlice(firstResult.startPos + xingOffset, 4);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
const word = readU32Be(slice);
|
||||
const isXing = word === XING || word === INFO;
|
||||
|
||||
if (isXing) {
|
||||
// Gotta be MP3
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -301,7 +305,6 @@ export class Mp3InputFormat extends InputFormat {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstHeader = firstResult.header;
|
||||
const secondHeader = secondResult.header;
|
||||
|
||||
// In a well-formed MP3 file, we'd expect these two frames to share some similarities:
|
||||
@@ -441,8 +444,23 @@ export class FlacInputFormat extends InputFormat {
|
||||
export class AdtsInputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
let currentPos = 0;
|
||||
|
||||
while (true) {
|
||||
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
let slice = input._reader.requestSliceRange(
|
||||
0,
|
||||
currentPos,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
MAX_ADTS_FRAME_HEADER_SIZE,
|
||||
);
|
||||
@@ -454,8 +472,10 @@ export class AdtsInputFormat extends InputFormat {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentPos += firstHeader.frameLength;
|
||||
|
||||
slice = input._reader.requestSliceRange(
|
||||
firstHeader.frameLength,
|
||||
currentPos,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
MAX_ADTS_FRAME_HEADER_SIZE,
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ export type PacketStats = {
|
||||
|
||||
export interface InputTrackBacking {
|
||||
getId(): number;
|
||||
getNumber(): number;
|
||||
getCodec(): MediaCodec | null;
|
||||
getInternalCodecId(): string | number | Uint8Array | null;
|
||||
getName(): string | null;
|
||||
@@ -94,6 +95,15 @@ export abstract class InputTrack {
|
||||
return this._backing.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* The 1-based index of this track among all tracks of the same type in the input file. For example, the first
|
||||
* video track has number 1, the second video track has number 2, and so on. The index refers to the order in
|
||||
* which the tracks are returned by {@link Input.getTracks}.
|
||||
*/
|
||||
get number() {
|
||||
return this._backing.getNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifier of the codec used internally by the container. It is not homogenized by Mediabunny
|
||||
* and depends entirely on the container format.
|
||||
|
||||
@@ -2335,6 +2335,25 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
return this.internalTrack.id;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
const demuxer = this.internalTrack.demuxer;
|
||||
const inputTrack = this.internalTrack.inputTrack!;
|
||||
const trackType = inputTrack.type;
|
||||
|
||||
let number = 0;
|
||||
for (const track of demuxer.tracks) {
|
||||
if (track.inputTrack!.type === trackType) {
|
||||
number++;
|
||||
}
|
||||
|
||||
if (track === this.internalTrack) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
getCodec(): MediaCodec | null {
|
||||
throw new Error('Not implemented on base class.');
|
||||
}
|
||||
|
||||
@@ -1859,6 +1859,27 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
return this.internalTrack.id;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
const demuxer = this.internalTrack.demuxer;
|
||||
const inputTrack = this.internalTrack.inputTrack!;
|
||||
const trackType = inputTrack.type;
|
||||
|
||||
let number = 0;
|
||||
for (const segment of demuxer.segments) {
|
||||
for (const track of segment.tracks) {
|
||||
if (track.inputTrack!.type === trackType) {
|
||||
number++;
|
||||
}
|
||||
|
||||
if (track === this.internalTrack) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
getCodec(): MediaCodec | null {
|
||||
throw new Error('Not implemented on base class.');
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,7 +17,7 @@
|
||||
* - For Ogg files, there is no global metadata so instead, the metadata refers to the combined metadata of all tracks,
|
||||
* in Vorbis-style comment headers.
|
||||
* - For WAVE files, the metadata refers to the chunks within the RIFF INFO chunk.
|
||||
* - For ADTS files, there is no metadata.
|
||||
* - For ADTS files, the metadata refers to the ID3v2 tags.
|
||||
* - For FLAC files, the metadata lives in Vorbis style in the Vorbis comment block.
|
||||
* - For MPEG-TS files, metadata tags are currently not supported.
|
||||
*
|
||||
@@ -70,6 +70,7 @@ export type MetadataTags = {
|
||||
* values. Additionally, all attached files (such as font files) are included here, where the key corresponds to
|
||||
* the FileUID and the value is an {@link AttachedFile}.
|
||||
* - MP3: The ID3v2 tags, or a single `'TAG'` key with the contents of the ID3v1 tag.
|
||||
* - ADTS: The ID3v2 tags.
|
||||
* - Ogg: The key-value string pairs from the Vorbis-style comment header (see RFC 7845, Section 5.2).
|
||||
* Additionally, the `'vendor'` key refers to the vendor string within this header.
|
||||
* - WAVE: The individual metadata chunks within the RIFF INFO chunk. Values are always ISO 8859-1 strings.
|
||||
|
||||
@@ -512,6 +512,10 @@ export const roundToMultiple = (value: number, multiple: number) => {
|
||||
return Math.round(value / multiple) * multiple;
|
||||
};
|
||||
|
||||
export const floorToMultiple = (value: number, multiple: number) => {
|
||||
return Math.floor(value / multiple) * multiple;
|
||||
};
|
||||
|
||||
export const ilog = (x: number) => {
|
||||
let ret = 0;
|
||||
while (x) {
|
||||
|
||||
@@ -217,6 +217,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
+558
-395
File diff suppressed because it is too large
Load Diff
@@ -428,6 +428,16 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return this.bitstream.serialNumber;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
// All Ogg tracks are audio, so the track's index + 1 is its number
|
||||
const index = this.demuxer.tracks.findIndex(
|
||||
t => (t._backing as OggAudioTrackBacking).bitstream === this.bitstream,
|
||||
);
|
||||
assert(index !== -1);
|
||||
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
getNumberOfChannels() {
|
||||
return this.bitstream.numberOfChannels;
|
||||
}
|
||||
|
||||
+20
-4
@@ -324,6 +324,9 @@ export type UrlSourceOptions = {
|
||||
/**
|
||||
* The [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) used by the Fetch API. Can be
|
||||
* used to further control the requests, such as setting custom headers.
|
||||
*
|
||||
* All fields will work except for `signal` and `headers.Range`; these will be overridden by Mediabunny. If you want
|
||||
* to cancel ongoing requests, use {@link Input.dispose}.
|
||||
*/
|
||||
requestInit?: RequestInit;
|
||||
|
||||
@@ -340,6 +343,9 @@ export type UrlSourceOptions = {
|
||||
/** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 64 MiB. */
|
||||
maxCacheSize?: number;
|
||||
|
||||
/** The maximum number of parallel requests to use for fetching. Defaults to 2. */
|
||||
parallelism?: number;
|
||||
|
||||
/**
|
||||
* A WHATWG-compatible fetch function. You can use this field to polyfill the `fetch` function, add missing
|
||||
* features, or use a custom implementation.
|
||||
@@ -368,7 +374,12 @@ export class UrlSource extends Source {
|
||||
abortController: AbortController;
|
||||
}>();
|
||||
|
||||
/** Creates a new {@link UrlSource} backed by the resource at the specified URL. */
|
||||
/**
|
||||
* Creates a new {@link UrlSource} backed by the resource at the specified URL.
|
||||
*
|
||||
* When passing a `Request` instance, note that the `signal` and `headers.Range` options will be overridden by
|
||||
* Mediabunny. If you want to cancel ongoing requests, use {@link Input.dispose}.
|
||||
*/
|
||||
constructor(
|
||||
url: string | URL | Request,
|
||||
options: UrlSourceOptions = {},
|
||||
@@ -395,6 +406,9 @@ export class UrlSource extends Source {
|
||||
) {
|
||||
throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.');
|
||||
}
|
||||
if (options.parallelism !== undefined && (!Number.isInteger(options.parallelism) || options.parallelism < 1)) {
|
||||
throw new TypeError('options.parallelism, when provided, must be a positive number.');
|
||||
}
|
||||
if (options.fetchFn !== undefined && typeof options.fetchFn !== 'function') {
|
||||
throw new TypeError('options.fetchFn, when provided, must be a function.');
|
||||
// Won't bother validating this function beyond this
|
||||
@@ -406,11 +420,13 @@ export class UrlSource extends Source {
|
||||
this._options = options;
|
||||
this._getRetryDelay = options.getRetryDelay ?? DEFAULT_RETRY_DELAY;
|
||||
|
||||
// Most files in the real-world have a single sequential access pattern, but having two in parallel can
|
||||
// also happen
|
||||
const DEFAULT_PARALLELISM = 2;
|
||||
|
||||
this._orchestrator = new ReadOrchestrator({
|
||||
maxCacheSize: options.maxCacheSize ?? (64 * 2 ** 20 /* 64 MiB */),
|
||||
// Most files in the real-world have a single sequential access pattern, but having two in parallel can
|
||||
// also happen
|
||||
maxWorkerCount: 2,
|
||||
maxWorkerCount: options.parallelism ?? DEFAULT_PARALLELISM,
|
||||
runWorker: this._runWorker.bind(this),
|
||||
prefetchProfile: PREFETCH_PROFILES.network,
|
||||
});
|
||||
|
||||
@@ -356,6 +356,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getCodec() {
|
||||
return this.demuxer.getCodec();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
|
||||
import { VideoSample } from '../../src/sample.js';
|
||||
import { Conversion } from '../../src/conversion.js';
|
||||
|
||||
test('Can decode transparent video', async () => {
|
||||
test.skip('Can decode transparent video', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
@@ -35,7 +35,7 @@ test('Can decode transparent video', async () => {
|
||||
expect(imageData.data[3]).toBeLessThan(255); // Check that there's actually transparent pixels
|
||||
});
|
||||
|
||||
test('Can decode faulty transparent video and behaves gracefully', async () => {
|
||||
test.skip('Can decode faulty transparent video and behaves gracefully', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency-faulty.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
@@ -55,7 +55,7 @@ test('Can decode faulty transparent video and behaves gracefully', async () => {
|
||||
expect(secondSample.hasAlpha).toBe(false);
|
||||
});
|
||||
|
||||
test('Can extract transparent frames via CanvasSink', async () => {
|
||||
test.skip('Can extract transparent frames via CanvasSink', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
@@ -81,7 +81,7 @@ test('Can extract transparent frames via CanvasSink', async () => {
|
||||
expect(imageData.data[3]).toBe(255);
|
||||
});
|
||||
|
||||
test('Can encode transparent video', async () => {
|
||||
test.skip('Can encode transparent video', async () => {
|
||||
const output = new Output({
|
||||
format: new WebMOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
@@ -175,7 +175,7 @@ test('Can encode transparent video', async () => {
|
||||
expect(imageData.data[3]).toBe(0); // Transparent
|
||||
});
|
||||
|
||||
test('Can encode video with alternating transparency', async () => {
|
||||
test.skip('Can encode video with alternating transparency', async () => {
|
||||
const output = new Output({
|
||||
format: new WebMOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
@@ -246,7 +246,7 @@ test('Can encode video with alternating transparency', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('Can encode transparent video with odd dimensions', async () => {
|
||||
test.skip('Can encode transparent video with odd dimensions', async () => {
|
||||
const output = new Output({
|
||||
format: new WebMOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
@@ -269,12 +269,12 @@ test('Can encode transparent video with odd dimensions', async () => {
|
||||
await output.finalize();
|
||||
});
|
||||
|
||||
test('Positive encodability check with alpha', async () => {
|
||||
test.skip('Positive encodability check with alpha', async () => {
|
||||
const result = await canEncodeVideo('vp9', { alpha: 'keep' });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('Can transmux transparent video, discards alpha by default', async () => {
|
||||
test.skip('Can transmux transparent video, discards alpha by default', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
@@ -303,7 +303,7 @@ test('Can transmux transparent video, discards alpha by default', async () => {
|
||||
expect(sample.hasAlpha).toBe(false);
|
||||
});
|
||||
|
||||
test('Can transmux transparent video, can keep alpha', async () => {
|
||||
test.skip('Can transmux transparent video, can keep alpha', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
@@ -336,7 +336,7 @@ test('Can transmux transparent video, can keep alpha', async () => {
|
||||
expect(sample.hasAlpha).toBe(true);
|
||||
});
|
||||
|
||||
test('Can reencode transparent video, keeping alpha', async () => {
|
||||
test.skip('Can reencode transparent video, keeping alpha', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
|
||||
@@ -62,7 +62,7 @@ test('ADTS muxer with raw AAC input', async () => {
|
||||
expect(count).toBe(237);
|
||||
});
|
||||
|
||||
test('ADTS muxer with ADTS input (passthrough)', async () => {
|
||||
test('ADTS muxer with ADTS input (passthrough)', { timeout: 10_000 }, async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/sample3.aac')),
|
||||
formats: ALL_FORMATS,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Output } from '../../src/output.js';
|
||||
import {
|
||||
AdtsOutputFormat,
|
||||
FlacOutputFormat,
|
||||
MkvOutputFormat,
|
||||
MovOutputFormat,
|
||||
@@ -478,6 +479,61 @@ test('Read and write metadata, FLAC', async () => {
|
||||
expect(readTags.raw!['COMPOSER']).toBe('Hans Zimmer');
|
||||
});
|
||||
|
||||
test('Read and write metadata, ADTS', async () => {
|
||||
const originalInput = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/sample3.aac')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const output = new Output({
|
||||
format: new AdtsOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
input: originalInput,
|
||||
output,
|
||||
tags: {
|
||||
...songMetadata,
|
||||
raw: {
|
||||
TXXY: 'ID3v2 goated',
|
||||
},
|
||||
},
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
using input = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const readTags = await input.getMetadataTags();
|
||||
|
||||
// ID3v2 is goated, so pretty much everything was copied:
|
||||
expect(readTags.title).toBe(songMetadata.title);
|
||||
expect(readTags.description).toBe(songMetadata.description);
|
||||
expect(readTags.artist).toBe(songMetadata.artist);
|
||||
expect(readTags.album).toBe(songMetadata.album);
|
||||
expect(readTags.albumArtist).toBe(songMetadata.albumArtist);
|
||||
expect(readTags.comment).toBe(songMetadata.comment);
|
||||
expect(readTags.lyrics).toBe(songMetadata.lyrics);
|
||||
expect(readTags.trackNumber).toBe(songMetadata.trackNumber);
|
||||
expect(readTags.tracksTotal).toBe(songMetadata.tracksTotal);
|
||||
expect(readTags.discNumber).toBe(songMetadata.discNumber);
|
||||
expect(readTags.discsTotal).toBe(songMetadata.discsTotal);
|
||||
expect(readTags.date).toEqual(readTags.date);
|
||||
expect(readTags.images).toHaveLength(1);
|
||||
expect(readTags.images![0]!.data).toEqual(coverArt);
|
||||
expect(readTags.images![0]!.mimeType).toEqual('image/jpeg');
|
||||
expect(readTags.images![0]!.kind).toEqual('coverFront');
|
||||
expect(readTags.images![0]!.description).toEqual(songMetadata.images![0]!.description);
|
||||
expect(readTags.images![0]!.name).toBeUndefined(); // Can't be contained in ID3v2
|
||||
|
||||
expect(readTags.raw!['TIT2']).toBe(songMetadata.title);
|
||||
expect(readTags.raw!['APIC']).instanceOf(Uint8Array);
|
||||
expect(readTags.raw!['TXXY']).toBe('ID3v2 goated');
|
||||
});
|
||||
|
||||
test('Read and write metadata, WAVE', async () => {
|
||||
const output = new Output({
|
||||
format: new WavOutputFormat(),
|
||||
|
||||
+302
-206
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { FilePathSource, ReadableStreamSource, UrlSource } from '../../src/source.js';
|
||||
import { FilePathSource, ReadableStreamSource, StreamSource } from '../../src/source.js';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { Readable } from 'node:stream';
|
||||
@@ -8,7 +8,7 @@ import { ALL_FORMATS, MPEG_TS } from '../../src/input-format.js';
|
||||
import { assert } from '../../src/misc.js';
|
||||
import { EncodedPacketSink } from '../../src/media-sink.js';
|
||||
import { EncodedPacket } from '../../src/packet.js';
|
||||
import { MpegTsTrackBacking } from '../../src/mpeg-ts/mpeg-ts-demuxer.js';
|
||||
import { MpegTsDemuxer } from '../../src/mpeg-ts/mpeg-ts-demuxer.js';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
|
||||
@@ -200,291 +200,329 @@ test('MPEG-TS AAC audio packets', async () => {
|
||||
});
|
||||
|
||||
test('MPEG-TS video seeking', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
if (i === 1) {
|
||||
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
|
||||
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
|
||||
}
|
||||
|
||||
const firstTimestamp = await videoTrack.getFirstTimestamp();
|
||||
const firstPacket = await sink.getPacket(firstTimestamp);
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.timestamp).toBe(firstTimestamp);
|
||||
expect(firstPacket.duration).toBe(0.016666666666666666);
|
||||
expect(firstPacket.sequenceNumber).toBe((await sink.getFirstPacket())?.sequenceNumber);
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
|
||||
const lastPacket = await sink.getPacket(Infinity);
|
||||
assert(lastPacket);
|
||||
expect(lastPacket.timestamp).toBeCloseTo(14.983333333333333);
|
||||
expect(lastPacket.duration).toBe(0.016666666666666666);
|
||||
const firstTimestamp = await videoTrack.getFirstTimestamp();
|
||||
const firstPacket = await sink.getPacket(firstTimestamp);
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.timestamp).toBe(firstTimestamp);
|
||||
expect(firstPacket.duration).toBe(0.016666666666666666);
|
||||
expect(firstPacket.sequenceNumber).toBe((await sink.getFirstPacket())?.sequenceNumber);
|
||||
|
||||
const beforeFirst = await sink.getPacket(-10);
|
||||
expect(beforeFirst).toBeNull();
|
||||
const lastPacket = await sink.getPacket(Infinity);
|
||||
assert(lastPacket);
|
||||
expect(lastPacket.timestamp).toBeCloseTo(14.983333333333333);
|
||||
expect(lastPacket.duration).toBe(0.016666666666666666);
|
||||
|
||||
const middlePacket = await sink.getPacket(12.5);
|
||||
assert(middlePacket);
|
||||
expect(middlePacket.timestamp).toBeCloseTo(12.5);
|
||||
expect(middlePacket.duration).toBe(0.016666666666666666);
|
||||
const beforeFirst = await sink.getPacket(-10);
|
||||
expect(beforeFirst).toBeNull();
|
||||
|
||||
const allPackets: EncodedPacket[] = [];
|
||||
let currentPacket: EncodedPacket | null = firstPacket;
|
||||
const middlePacket = await sink.getPacket(12.5);
|
||||
assert(middlePacket);
|
||||
expect(middlePacket.timestamp).toBeCloseTo(12.5);
|
||||
expect(middlePacket.duration).toBe(0.016666666666666666);
|
||||
|
||||
while (currentPacket) {
|
||||
allPackets.push(currentPacket);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
const allPackets: EncodedPacket[] = [];
|
||||
let currentPacket: EncodedPacket | null = firstPacket;
|
||||
|
||||
expect(allPackets).toHaveLength(298);
|
||||
while (currentPacket) {
|
||||
allPackets.push(currentPacket);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
|
||||
for (const packet of allPackets) {
|
||||
const seekedPacked = await sink.getPacket(packet.timestamp);
|
||||
assert(seekedPacked);
|
||||
expect(seekedPacked.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet
|
||||
expect(seekedPacked.duration).toBe(packet.duration); // The correct duration was retrieved for this packet
|
||||
expect(seekedPacked.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
expect(allPackets).toHaveLength(298);
|
||||
|
||||
for (const packet of allPackets) {
|
||||
const seekedPacket = await sink.getPacket(packet.timestamp);
|
||||
assert(seekedPacket);
|
||||
expect(seekedPacket.timestamp).toBe(packet.timestamp);
|
||||
expect(seekedPacket.duration).toBe(packet.duration);
|
||||
expect(seekedPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS audio seeking', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(audioTrack);
|
||||
if (i === 1) {
|
||||
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
|
||||
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
|
||||
}
|
||||
|
||||
const firstTimestamp = await audioTrack.getFirstTimestamp();
|
||||
const firstPacket = await sink.getPacket(firstTimestamp);
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.timestamp).toBe(firstTimestamp);
|
||||
expect(firstPacket.duration).toBe(0.021333333333333333);
|
||||
expect(firstPacket.sequenceNumber).toBe((await sink.getFirstPacket())?.sequenceNumber);
|
||||
const sink = new EncodedPacketSink(audioTrack);
|
||||
|
||||
const lastPacket = await sink.getPacket(Infinity);
|
||||
assert(lastPacket);
|
||||
expect(lastPacket.timestamp).toBeCloseTo(14.982666666666667);
|
||||
expect(lastPacket.duration).toBe(0.021333333333333333);
|
||||
const firstTimestamp = await audioTrack.getFirstTimestamp();
|
||||
const firstPacket = await sink.getPacket(firstTimestamp);
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.timestamp).toBe(firstTimestamp);
|
||||
expect(firstPacket.duration).toBe(0.021333333333333333);
|
||||
expect(firstPacket.sequenceNumber).toBe((await sink.getFirstPacket())?.sequenceNumber);
|
||||
|
||||
const beforeFirst = await sink.getPacket(-10);
|
||||
expect(beforeFirst).toBeNull();
|
||||
const lastPacket = await sink.getPacket(Infinity);
|
||||
assert(lastPacket);
|
||||
expect(lastPacket.timestamp).toBeCloseTo(14.982666666666667);
|
||||
expect(lastPacket.duration).toBe(0.021333333333333333);
|
||||
|
||||
const middlePacket = await sink.getPacket(12.5);
|
||||
assert(middlePacket);
|
||||
expect(middlePacket.timestamp).toBeCloseTo(12.486666666666666);
|
||||
expect(middlePacket.duration).toBe(0.021333333333333333);
|
||||
const beforeFirst = await sink.getPacket(-10);
|
||||
expect(beforeFirst).toBeNull();
|
||||
|
||||
const allPackets: EncodedPacket[] = [];
|
||||
let currentPacket: EncodedPacket | null = firstPacket;
|
||||
const middlePacket = await sink.getPacket(12.5);
|
||||
assert(middlePacket);
|
||||
expect(middlePacket.timestamp).toBeCloseTo(12.486666666666666);
|
||||
expect(middlePacket.duration).toBe(0.021333333333333333);
|
||||
|
||||
while (currentPacket) {
|
||||
allPackets.push(currentPacket);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
const allPackets: EncodedPacket[] = [];
|
||||
let currentPacket: EncodedPacket | null = firstPacket;
|
||||
|
||||
expect(allPackets).toHaveLength(234);
|
||||
while (currentPacket) {
|
||||
allPackets.push(currentPacket);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
|
||||
for (const packet of allPackets) {
|
||||
const seekedPacket = await sink.getPacket(packet.timestamp);
|
||||
assert(seekedPacket);
|
||||
expect(seekedPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet
|
||||
expect(seekedPacket.duration).toBe(packet.duration); // The correct duration was retrieved for this packet
|
||||
expect(seekedPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
expect(allPackets).toHaveLength(234);
|
||||
|
||||
for (const packet of allPackets) {
|
||||
const seekedPacket = await sink.getPacket(packet.timestamp);
|
||||
assert(seekedPacket);
|
||||
expect(seekedPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved
|
||||
expect(seekedPacket.duration).toBe(packet.duration); // The correct duration was retrieved
|
||||
expect(seekedPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS seeking race condition test', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
if (i === 1) {
|
||||
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
|
||||
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
|
||||
}
|
||||
|
||||
const allPackets: EncodedPacket[] = [];
|
||||
let currentPacket: EncodedPacket | null = await sink.getFirstPacket();
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
|
||||
while (currentPacket) {
|
||||
allPackets.push(currentPacket);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
const allPackets: EncodedPacket[] = [];
|
||||
let currentPacket: EncodedPacket | null = await sink.getFirstPacket();
|
||||
|
||||
// Perform all seeks concurrently
|
||||
const seekPromises = allPackets.map(packet => sink.getPacket(packet.timestamp));
|
||||
const seekedPackets = await Promise.all(seekPromises);
|
||||
while (currentPacket) {
|
||||
allPackets.push(currentPacket);
|
||||
currentPacket = await sink.getNextPacket(currentPacket);
|
||||
}
|
||||
|
||||
for (let i = 0; i < allPackets.length; i++) {
|
||||
const originalPacket = allPackets[i]!;
|
||||
const seekedPacket = seekedPackets[i]!;
|
||||
assert(seekedPacket);
|
||||
expect(seekedPacket.timestamp).toBe(originalPacket.timestamp);
|
||||
expect(seekedPacket.duration).toBe(originalPacket.duration);
|
||||
expect(seekedPacket.sequenceNumber).toBe(originalPacket.sequenceNumber);
|
||||
// Perform all seeks concurrently
|
||||
const seekPromises = allPackets.map(packet => sink.getPacket(packet.timestamp));
|
||||
const seekedPackets = await Promise.all(seekPromises);
|
||||
|
||||
for (let j = 0; j < allPackets.length; j++) {
|
||||
const originalPacket = allPackets[j]!;
|
||||
const seekedPacket = seekedPackets[j]!;
|
||||
assert(seekedPacket);
|
||||
expect(seekedPacket.timestamp).toBe(originalPacket.timestamp);
|
||||
expect(seekedPacket.duration).toBe(originalPacket.duration);
|
||||
expect(seekedPacket.sequenceNumber).toBe(originalPacket.sequenceNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS video key packets', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/193039199_mp4_h264_aac_fhd_7.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny-ffmpeg.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
if (i === 1) {
|
||||
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
|
||||
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
|
||||
}
|
||||
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
|
||||
const secondPacket = await sink.getNextPacket(firstPacket);
|
||||
assert(secondPacket);
|
||||
expect(secondPacket.type).toBe('delta');
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
|
||||
const nextKeyPacket = await sink.getNextKeyPacket(firstPacket);
|
||||
assert(nextKeyPacket);
|
||||
expect(nextKeyPacket.type).toBe('key');
|
||||
expect(nextKeyPacket.sequenceNumber).toBeGreaterThan(secondPacket.sequenceNumber);
|
||||
const secondPacket = await sink.getNextPacket(firstPacket);
|
||||
assert(secondPacket);
|
||||
expect(secondPacket.type).toBe('delta');
|
||||
|
||||
const firstKeyPacket = await sink.getKeyPacket(firstPacket.timestamp + 1.0);
|
||||
assert(firstKeyPacket);
|
||||
expect(firstKeyPacket.type).toBe('key');
|
||||
expect(firstKeyPacket.sequenceNumber).toBe(firstPacket.sequenceNumber);
|
||||
const nextKeyPacket = await sink.getNextKeyPacket(firstPacket);
|
||||
assert(nextKeyPacket);
|
||||
expect(nextKeyPacket.type).toBe('key');
|
||||
expect(nextKeyPacket.sequenceNumber).toBeGreaterThan(secondPacket.sequenceNumber);
|
||||
|
||||
const afterKeyPacket = await sink.getNextPacket(firstKeyPacket);
|
||||
expect(afterKeyPacket).not.toBe(null);
|
||||
expect(afterKeyPacket!.type).toBe('delta');
|
||||
expect(afterKeyPacket!.sequenceNumber).toBe(secondPacket.sequenceNumber);
|
||||
const firstKeyPacket = await sink.getKeyPacket(firstPacket.timestamp + 0.5);
|
||||
assert(firstKeyPacket);
|
||||
expect(firstKeyPacket.type).toBe('key');
|
||||
expect(firstKeyPacket.sequenceNumber).toBe(firstPacket.sequenceNumber);
|
||||
|
||||
const secondKeyPacket = await sink.getKeyPacket(15);
|
||||
assert(secondKeyPacket);
|
||||
expect(secondKeyPacket.type).toBe('key');
|
||||
expect(secondKeyPacket.sequenceNumber).toBe(nextKeyPacket.sequenceNumber);
|
||||
const afterKeyPacket = await sink.getNextPacket(firstKeyPacket);
|
||||
expect(afterKeyPacket).not.toBe(null);
|
||||
expect(afterKeyPacket!.type).toBe('delta');
|
||||
expect(afterKeyPacket!.sequenceNumber).toBe(secondPacket.sequenceNumber);
|
||||
|
||||
const lastKeyPacket = await sink.getKeyPacket(Infinity);
|
||||
assert(lastKeyPacket);
|
||||
expect(lastKeyPacket.type).toBe('key');
|
||||
expect(lastKeyPacket.sequenceNumber).toBe(secondKeyPacket.sequenceNumber); // It's actually the last key packet
|
||||
const secondKeyPacket = await sink.getKeyPacket(2.5);
|
||||
assert(secondKeyPacket);
|
||||
expect(secondKeyPacket.type).toBe('key');
|
||||
expect(secondKeyPacket.sequenceNumber).toBe(nextKeyPacket.sequenceNumber);
|
||||
|
||||
const allKeyPackets: EncodedPacket[] = [];
|
||||
let currentKeyPacket: EncodedPacket | null = firstPacket;
|
||||
const lastKeyPacket = await sink.getKeyPacket(Infinity);
|
||||
assert(lastKeyPacket);
|
||||
expect(lastKeyPacket.type).toBe('key');
|
||||
expect(lastKeyPacket.sequenceNumber).toBeGreaterThan(secondKeyPacket.sequenceNumber);
|
||||
|
||||
while (currentKeyPacket) {
|
||||
allKeyPackets.push(currentKeyPacket);
|
||||
currentKeyPacket = await sink.getNextKeyPacket(currentKeyPacket);
|
||||
}
|
||||
const allKeyPackets: EncodedPacket[] = [];
|
||||
let currentKeyPacket: EncodedPacket | null = firstPacket;
|
||||
|
||||
for (const packet of allKeyPackets) {
|
||||
const keyPacket = await sink.getKeyPacket(packet.timestamp);
|
||||
assert(keyPacket);
|
||||
expect(keyPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet
|
||||
expect(keyPacket.duration).toBe(packet.duration); // The correct duration was retrieved for this packet
|
||||
expect(keyPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
while (currentKeyPacket) {
|
||||
allKeyPackets.push(currentKeyPacket);
|
||||
currentKeyPacket = await sink.getNextKeyPacket(currentKeyPacket);
|
||||
}
|
||||
|
||||
for (const packet of allKeyPackets) {
|
||||
const keyPacket = await sink.getKeyPacket(packet.timestamp);
|
||||
assert(keyPacket);
|
||||
expect(keyPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet
|
||||
expect(keyPacket.duration).toBe(packet.duration); // The correct duration was retrieved for this packet
|
||||
expect(keyPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS audio key packets', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/0.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
assert(audioTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(audioTrack);
|
||||
if (i === 1) {
|
||||
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
|
||||
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
|
||||
}
|
||||
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
const sink = new EncodedPacketSink(audioTrack);
|
||||
|
||||
const secondPacket = await sink.getNextPacket(firstPacket);
|
||||
assert(secondPacket);
|
||||
expect(secondPacket.type).toBe('key');
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
|
||||
const nextKeyPacket = await sink.getNextKeyPacket(firstPacket);
|
||||
assert(nextKeyPacket);
|
||||
expect(nextKeyPacket.type).toBe('key');
|
||||
expect(nextKeyPacket.sequenceNumber).toBe(secondPacket.sequenceNumber); // All audio packets are key packets
|
||||
const secondPacket = await sink.getNextPacket(firstPacket);
|
||||
assert(secondPacket);
|
||||
expect(secondPacket.type).toBe('key');
|
||||
|
||||
const middleKeyPacket = await sink.getKeyPacket(12.5);
|
||||
assert(middleKeyPacket);
|
||||
expect(middleKeyPacket.type).toBe('key');
|
||||
const nextKeyPacket = await sink.getNextKeyPacket(firstPacket);
|
||||
assert(nextKeyPacket);
|
||||
expect(nextKeyPacket.type).toBe('key');
|
||||
expect(nextKeyPacket.sequenceNumber).toBe(secondPacket.sequenceNumber); // All audio packets are key packets
|
||||
|
||||
const afterKeyPacket = await sink.getNextPacket(middleKeyPacket);
|
||||
expect(afterKeyPacket).not.toBe(null);
|
||||
expect(afterKeyPacket!.type).toBe('key');
|
||||
const middleKeyPacket = await sink.getKeyPacket(12.5);
|
||||
assert(middleKeyPacket);
|
||||
expect(middleKeyPacket.type).toBe('key');
|
||||
|
||||
const lastPacket = await sink.getPacket(Infinity);
|
||||
assert(lastPacket);
|
||||
const afterKeyPacket = await sink.getNextPacket(middleKeyPacket);
|
||||
expect(afterKeyPacket).not.toBe(null);
|
||||
expect(afterKeyPacket!.type).toBe('key');
|
||||
|
||||
const lastKeyPacket = await sink.getKeyPacket(Infinity);
|
||||
assert(lastKeyPacket);
|
||||
expect(lastKeyPacket.type).toBe('key');
|
||||
expect(lastKeyPacket.sequenceNumber).toBe(lastPacket.sequenceNumber); // It's actually the last packet
|
||||
const lastPacket = await sink.getPacket(Infinity);
|
||||
assert(lastPacket);
|
||||
|
||||
const allKeyPackets: EncodedPacket[] = [];
|
||||
let currentKeyPacket: EncodedPacket | null = firstPacket;
|
||||
const lastKeyPacket = await sink.getKeyPacket(Infinity);
|
||||
assert(lastKeyPacket);
|
||||
expect(lastKeyPacket.type).toBe('key');
|
||||
expect(lastKeyPacket.sequenceNumber).toBe(lastPacket.sequenceNumber); // It's actually the last packet
|
||||
|
||||
while (currentKeyPacket) {
|
||||
allKeyPackets.push(currentKeyPacket);
|
||||
currentKeyPacket = await sink.getNextKeyPacket(currentKeyPacket);
|
||||
}
|
||||
const allKeyPackets: EncodedPacket[] = [];
|
||||
let currentKeyPacket: EncodedPacket | null = firstPacket;
|
||||
|
||||
for (const packet of allKeyPackets) {
|
||||
const keyPacket = await sink.getKeyPacket(packet.timestamp);
|
||||
assert(keyPacket);
|
||||
expect(keyPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet
|
||||
expect(keyPacket.duration).toBe(packet.duration); // The correct duration was retrieved for this packet
|
||||
expect(keyPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
while (currentKeyPacket) {
|
||||
allKeyPackets.push(currentKeyPacket);
|
||||
currentKeyPacket = await sink.getNextKeyPacket(currentKeyPacket);
|
||||
}
|
||||
|
||||
for (const packet of allKeyPackets) {
|
||||
const keyPacket = await sink.getKeyPacket(packet.timestamp);
|
||||
assert(keyPacket);
|
||||
expect(keyPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet
|
||||
expect(keyPacket.duration).toBe(packet.duration); // The correct duration was retrieved for this packet
|
||||
expect(keyPacket.sequenceNumber).toBe(packet.sequenceNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS with unknown file size (ReadableStreamSource)', async () => {
|
||||
const filePath = path.join(__dirname, '../public/0.ts');
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const webStream = Readable.toWeb(fileStream) as ReadableStream<Uint8Array>;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const filePath = path.join(__dirname, '../public/0.ts');
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const webStream = Readable.toWeb(fileStream) as ReadableStream<Uint8Array>;
|
||||
|
||||
using input = new Input({
|
||||
source: new ReadableStreamSource(webStream),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
using input = new Input({
|
||||
source: new ReadableStreamSource(webStream),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
if (i === 1) {
|
||||
const demuxer = (await input._demuxerPromise) as MpegTsDemuxer;
|
||||
demuxer.seekChunkSize = 250_000; // Try it again with a smaller chunk size
|
||||
}
|
||||
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
expect(firstPacket.timestamp).toBe(10.033333333333333);
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
|
||||
const middlePacket = await sink.getPacket(12.5);
|
||||
assert(middlePacket);
|
||||
expect(middlePacket.timestamp).toBeCloseTo(12.5);
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
expect(firstPacket.timestamp).toBe(10.033333333333333);
|
||||
|
||||
const duration = await videoTrack.computeDuration();
|
||||
expect(duration).toBeCloseTo(15);
|
||||
const middlePacket = await sink.getPacket(12.5);
|
||||
assert(middlePacket);
|
||||
expect(middlePacket.timestamp).toBeCloseTo(12.5);
|
||||
|
||||
// Ensure that reference points have still been added
|
||||
expect((videoTrack._backing as unknown as MpegTsTrackBacking).referencePesPackets.length)
|
||||
.toBeGreaterThanOrEqual(10);
|
||||
const duration = await videoTrack.computeDuration();
|
||||
expect(duration).toBeCloseTo(15);
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS transmuxed by FFmpeg', { timeout: 30_000 }, async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/trim-buck-bunny-ffmpeg.ts'),
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny-ffmpeg.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
@@ -573,3 +611,61 @@ test('MPEG-TS with MP3 audio', async () => {
|
||||
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('MPEG-TS partial reading', async () => {
|
||||
const fullPath = path.join(__dirname, '../public/193039199_mp4_h264_aac_fhd_7.ts');
|
||||
const buffer = await fs.promises.readFile(fullPath);
|
||||
let maxEnd = 0;
|
||||
|
||||
using input = new Input({
|
||||
source: new StreamSource({
|
||||
getSize: () => {
|
||||
return buffer.byteLength;
|
||||
},
|
||||
read: (start, end) => {
|
||||
maxEnd = Math.max(maxEnd, end);
|
||||
return buffer.subarray(start, end);
|
||||
},
|
||||
}),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
await input.getTracks();
|
||||
|
||||
// Not much of the file has been read since we only requested metadata
|
||||
expect(maxEnd).toBeLessThanOrEqual(86480);
|
||||
});
|
||||
|
||||
test('MPEG-TS first packet key packet forcing', async () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/missing-keyframe-labels.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);
|
||||
|
||||
// Test that the first packet is indeed a key packet even if the file doesn't label it that way (L muxer)
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.type).toBe('key');
|
||||
|
||||
const firstPacketSeeked = await sink.getPacket(firstPacket.timestamp);
|
||||
assert(firstPacketSeeked);
|
||||
expect(firstPacketSeeked.type).toBe('key');
|
||||
expect(firstPacketSeeked.sequenceNumber).toBe(firstPacket.sequenceNumber);
|
||||
|
||||
const firstKeyPacketSeeked = await sink.getKeyPacket(firstPacket.timestamp);
|
||||
assert(firstKeyPacketSeeked);
|
||||
expect(firstKeyPacketSeeked.type).toBe('key');
|
||||
expect(firstKeyPacketSeeked.sequenceNumber).toBe(firstPacket.sequenceNumber);
|
||||
}
|
||||
});
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user