mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-04 06:13:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cacbc2a399 | ||
|
|
ab0fd6a8d8 | ||
|
|
6a099b4259 | ||
|
|
172b4a1fd2 | ||
|
|
43c1ea7efd | ||
|
|
cab92c55ec | ||
|
|
81bc9d7c44 | ||
|
|
3209363470 | ||
|
|
d16b1927ad | ||
|
|
f620a328e5 | ||
|
|
7aa0fa465e | ||
|
|
cc5f7d8e8f |
@@ -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>
|
||||
|
||||
@@ -506,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' },
|
||||
@@ -121,6 +123,7 @@ const sponsors = {
|
||||
{ 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 |
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.30.0",
|
||||
"version": "1.32.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.30.0",
|
||||
"version": "1.32.2",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7739,9 +7739,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.29.1",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.29.1.tgz",
|
||||
"integrity": "sha512-RrlKs69MxRGa/l9cMGeI4hzuTSgchOGFHk9lIAuu9EcbSdJ05gDbRsTY67dojAiyUP3ic4PExij5OqDxqSv82Q==",
|
||||
"version": "1.32.1",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.32.1.tgz",
|
||||
"integrity": "sha512-pPxhBEIY7dNEhRarN/DTVL56RTce0iCwNwkhu/at+S9S/HKc01PpHs8oM2I+1I9ztQH/bo2zdyszxDuAwsEBAg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12065,7 +12065,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.30.0",
|
||||
"version": "1.32.2",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.30.0",
|
||||
"version": "1.32.2",
|
||||
"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.30.0",
|
||||
"version": "1.32.2",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]!);
|
||||
|
||||
|
||||
+5
-1
@@ -801,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,
|
||||
);
|
||||
|
||||
|
||||
+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,
|
||||
);
|
||||
|
||||
+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.
|
||||
|
||||
+193
-166
@@ -63,8 +63,6 @@ import { EncodedPacket, PacketType, PLACEHOLDER_DATA } from '../packet';
|
||||
import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../reader';
|
||||
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
|
||||
|
||||
const MISSING_PES_PACKET_ERROR = 'No PES packet found where one was expected.';
|
||||
|
||||
type ElementaryStream = {
|
||||
demuxer: MpegTsDemuxer;
|
||||
pid: number;
|
||||
@@ -87,6 +85,11 @@ type ElementaryStream = {
|
||||
numberOfChannels: number;
|
||||
sampleRate: number;
|
||||
};
|
||||
/**
|
||||
* Reference PES packets, spread throughout the file, to be used to speed up repeated random access. Sorted by both
|
||||
* byte offset and PTS.
|
||||
*/
|
||||
referencePesPackets: PesPacketHeader[];
|
||||
};
|
||||
|
||||
type ElementaryVideoStream = ElementaryStream & { info: { type: 'video' } };
|
||||
@@ -163,6 +166,17 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
let hasProgramMap = false;
|
||||
|
||||
while (true) {
|
||||
const packetHeader = await this.readPacketHeader(currentPos);
|
||||
if (!packetHeader) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (packetHeader.payloadUnitStartIndicator === 0) {
|
||||
// Not the start of a section
|
||||
currentPos += this.packetStride;
|
||||
continue;
|
||||
}
|
||||
|
||||
const section = await this.readSection(
|
||||
currentPos,
|
||||
true,
|
||||
@@ -175,6 +189,26 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
const BYTES_BEFORE_SECTION_LENGTH = 3;
|
||||
const BITS_IN_CRC_32 = 32; // Duh
|
||||
|
||||
// Some streams don't contain a PAT for some reason, so we must do some guesswork to figure out where
|
||||
// the PMT is.
|
||||
let isProbablyProgramMap = false;
|
||||
if (!hasProgramMap && section.pid !== 0) {
|
||||
const isPesPacket
|
||||
= section.payload[0] === 0x00 && section.payload[1] === 0x00 && section.payload[2] === 0x01;
|
||||
|
||||
if (!isPesPacket) {
|
||||
// Assume it's a PSI
|
||||
|
||||
const bitstream = new Bitstream(section.payload);
|
||||
const pointerField = bitstream.readAlignedByte();
|
||||
|
||||
bitstream.skipBits(8 * pointerField);
|
||||
|
||||
const tableId = bitstream.readBits(8);
|
||||
isProbablyProgramMap = tableId === 0x02; // 0x02 == TS_program_map_section
|
||||
}
|
||||
}
|
||||
|
||||
if (section.pid === 0 && !hasProgramAssociationTable) {
|
||||
const bitstream = new Bitstream(section.payload);
|
||||
const pointerField = bitstream.readAlignedByte();
|
||||
@@ -204,7 +238,7 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
hasProgramAssociationTable = true;
|
||||
} else if (section.pid === programMapPid && !hasProgramMap) {
|
||||
} else if ((section.pid === programMapPid || isProbablyProgramMap) && !hasProgramMap) {
|
||||
const bitstream = new Bitstream(section.payload);
|
||||
const pointerField = bitstream.readAlignedByte();
|
||||
|
||||
@@ -275,6 +309,7 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
default: {
|
||||
// If we don't recognize the codec, we don't surface the track at all. This is because
|
||||
// we can't determine its metadata and also have no idea how to packetize its data.
|
||||
console.warn(`Unsupported stream_type 0x${streamType.toString(16)}; ignoring stream.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +321,7 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
initialized: false,
|
||||
firstSection: null,
|
||||
info,
|
||||
referencePesPackets: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -413,8 +449,15 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
break;
|
||||
}
|
||||
|
||||
assert(section.endPos !== null);
|
||||
currentPos = section.endPos;
|
||||
currentPos += this.packetStride;
|
||||
}
|
||||
|
||||
if (!hasProgramMap) {
|
||||
if (!hasProgramAssociationTable) {
|
||||
throw new Error('No Program Association Table found in the file.');
|
||||
}
|
||||
|
||||
throw new Error('No Program Map Table found in the file.');
|
||||
}
|
||||
|
||||
for (const stream of this.elementaryStreams) {
|
||||
@@ -651,6 +694,10 @@ type PesPacket = PesPacketHeader & {
|
||||
};
|
||||
|
||||
const readPesPacketHeader = (section: Section): PesPacketHeader | null => {
|
||||
if (section.payload.byteLength < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bitstream = new Bitstream(section.payload);
|
||||
|
||||
const startCodePrefix = bitstream.readBits(24);
|
||||
@@ -742,12 +789,6 @@ const readPesPacket = (section: Section): PesPacket | null => {
|
||||
};
|
||||
|
||||
export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
/**
|
||||
* Reference PES packets, spread throughout the file, to be used to speed up repeated random access. Sorted by both
|
||||
* byte offset and PTS.
|
||||
*/
|
||||
referencePesPackets: PesPacketHeader[] = [];
|
||||
endReferencePesPacketAdded = false;
|
||||
packetBuffers = new WeakMap<EncodedPacket, PacketBuffer>();
|
||||
/** Used for recreating PacketBuffers if necessary. */
|
||||
packetSectionStarts = new WeakMap<EncodedPacket, number>();
|
||||
@@ -812,7 +853,6 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
}
|
||||
|
||||
abstract allPacketsAreKeyPackets(): boolean;
|
||||
abstract markNextPacket(context: PacketReadingContext): Promise<void>;
|
||||
abstract getReorderSize(): number;
|
||||
|
||||
createEncodedPacket(
|
||||
@@ -840,45 +880,6 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
);
|
||||
}
|
||||
|
||||
maybeInsertReferencePacket(pesPacketHeader: PesPacketHeader) {
|
||||
const index = binarySearchLessOrEqual(
|
||||
this.referencePesPackets,
|
||||
pesPacketHeader.sectionStartPos,
|
||||
x => x.sectionStartPos,
|
||||
);
|
||||
if (index >= 0) {
|
||||
// Since pts and file position don't necessarily have a monotonic relationship (since pts can go crazy),
|
||||
// let's see if inserting at the given index would violate the pts order. If so, return.
|
||||
const entry = this.referencePesPackets[index]!;
|
||||
if (pesPacketHeader.pts <= entry.pts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const minByteDistance = this.elementaryStream.demuxer.minReferencePointByteDistance;
|
||||
|
||||
if (pesPacketHeader.sectionStartPos - entry.sectionStartPos < minByteDistance) {
|
||||
// Too close
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index < this.referencePesPackets.length - 1) {
|
||||
const nextEntry = this.referencePesPackets[index + 1]!;
|
||||
if (nextEntry.pts < pesPacketHeader.pts) {
|
||||
// Out of order
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextEntry.sectionStartPos - pesPacketHeader.sectionStartPos < minByteDistance) {
|
||||
// Too close
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.referencePesPackets.splice(index + 1, 0, pesPacketHeader);
|
||||
return true;
|
||||
}
|
||||
|
||||
async getFirstPacket(options: PacketRetrievalOptions): Promise<EncodedPacket | null> {
|
||||
const section = this.elementaryStream.firstSection;
|
||||
assert(section);
|
||||
@@ -886,7 +887,7 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const pesPacket = readPesPacket(section);
|
||||
assert(pesPacket);
|
||||
|
||||
const context = new PacketReadingContext(this, pesPacket, true);
|
||||
const context = new PacketReadingContext(this.elementaryStream, pesPacket);
|
||||
const buffer = new PacketBuffer(this, context);
|
||||
|
||||
const result = await buffer.readNext();
|
||||
@@ -936,7 +937,7 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const pesPacket = readPesPacket(section);
|
||||
assert(pesPacket);
|
||||
|
||||
const context = new PacketReadingContext(this, pesPacket, true);
|
||||
const context = new PacketReadingContext(this.elementaryStream, pesPacket);
|
||||
buffer = new PacketBuffer(this, context);
|
||||
|
||||
// Advance until we pass the current packet's sequence number
|
||||
@@ -1017,7 +1018,9 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
}
|
||||
|
||||
const pesPacketHeader = readPesPacketHeader(section);
|
||||
return pesPacketHeader;
|
||||
if (pesPacketHeader) {
|
||||
return pesPacketHeader;
|
||||
}
|
||||
}
|
||||
|
||||
currentPos += demuxer.packetStride;
|
||||
@@ -1038,8 +1041,10 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
}
|
||||
|
||||
let scanStartPos: number;
|
||||
const referencePointIndex = binarySearchLessOrEqual(this.referencePesPackets, searchPts, x => x.pts);
|
||||
const referencePoint = referencePointIndex !== -1 ? this.referencePesPackets[referencePointIndex]! : null;
|
||||
|
||||
const referencePesPackets = this.elementaryStream.referencePesPackets;
|
||||
const referencePointIndex = binarySearchLessOrEqual(referencePesPackets, searchPts, x => x.pts);
|
||||
const referencePoint = referencePointIndex !== -1 ? referencePesPackets[referencePointIndex]! : null;
|
||||
if (referencePoint && searchPts - referencePoint.pts < TIMESCALE / 2) {
|
||||
// Reference point ain't too far away, prefer it over the chunk search
|
||||
scanStartPos = referencePoint.sectionStartPos;
|
||||
@@ -1111,7 +1116,7 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const pesPacket = readPesPacket(section);
|
||||
assert(pesPacket);
|
||||
|
||||
const context = new PacketReadingContext(this, pesPacket, true);
|
||||
const context = new PacketReadingContext(this.elementaryStream, pesPacket);
|
||||
const buffer = new PacketBuffer(this, context);
|
||||
|
||||
// Advance until the top-most presentation timestamp crosses or equals searchPts
|
||||
@@ -1173,17 +1178,16 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const section = await demuxer.readSection(currentPos, false);
|
||||
if (section) {
|
||||
const nextPesHeader = readPesPacketHeader(section);
|
||||
if (!nextPesHeader) {
|
||||
throw new Error(MISSING_PES_PACKET_ERROR);
|
||||
}
|
||||
if (nextPesHeader.pts > searchPts) {
|
||||
break outer;
|
||||
}
|
||||
if (nextPesHeader) {
|
||||
if (nextPesHeader.pts > searchPts) {
|
||||
break outer;
|
||||
}
|
||||
|
||||
currentPesHeader = nextPesHeader;
|
||||
this.maybeInsertReferencePacket(nextPesHeader);
|
||||
currentPesHeader = nextPesHeader;
|
||||
maybeInsertReferencePacket(this.elementaryStream, nextPesHeader);
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1206,12 +1210,10 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const section = await demuxer.readSection(pos, false);
|
||||
if (section) {
|
||||
const header = readPesPacketHeader(section);
|
||||
if (!header) {
|
||||
throw new Error(MISSING_PES_PACKET_ERROR);
|
||||
if (header) {
|
||||
currentPesHeader = header;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPesHeader = header;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1286,13 +1288,10 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const section = await demuxer.readSection(currentPos, false);
|
||||
if (section) {
|
||||
pesHeader = readPesPacketHeader(section);
|
||||
if (!pesHeader) {
|
||||
throw new Error(MISSING_PES_PACKET_ERROR);
|
||||
if (pesHeader) {
|
||||
maybeInsertReferencePacket(this.elementaryStream, pesHeader);
|
||||
break;
|
||||
}
|
||||
|
||||
this.maybeInsertReferencePacket(pesHeader);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1319,12 +1318,10 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
const section = await demuxer.readSection(pos, false);
|
||||
if (section) {
|
||||
const header = readPesPacketHeader(section);
|
||||
if (!header) {
|
||||
throw new Error(MISSING_PES_PACKET_ERROR);
|
||||
if (header) {
|
||||
startPesHeader = header;
|
||||
break;
|
||||
}
|
||||
|
||||
startPesHeader = header;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1420,11 +1417,97 @@ class MpegTsVideoTrackBacking extends MpegTsTrackBacking implements InputVideoTr
|
||||
override getReorderSize(): number {
|
||||
return this.elementaryStream.info.reorderSize;
|
||||
}
|
||||
}
|
||||
|
||||
override async markNextPacket(context: PacketReadingContext): Promise<void> {
|
||||
assert(!context.suppliedPacket);
|
||||
class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTrackBacking {
|
||||
override elementaryStream: ElementaryAudioStream;
|
||||
|
||||
const codec = this.elementaryStream.info.codec;
|
||||
constructor(elementaryStream: ElementaryAudioStream) {
|
||||
super(elementaryStream);
|
||||
this.elementaryStream = elementaryStream;
|
||||
}
|
||||
|
||||
override getCodec(): AudioCodec {
|
||||
return this.elementaryStream.info.codec;
|
||||
}
|
||||
|
||||
getNumberOfChannels() {
|
||||
return this.elementaryStream.info.numberOfChannels;
|
||||
}
|
||||
|
||||
getSampleRate() {
|
||||
return this.elementaryStream.info.sampleRate;
|
||||
}
|
||||
|
||||
async getDecoderConfig(): Promise<AudioDecoderConfig> {
|
||||
return {
|
||||
codec: extractAudioCodecString({
|
||||
codec: this.elementaryStream.info.codec,
|
||||
codecDescription: null,
|
||||
aacCodecInfo: this.elementaryStream.info.aacCodecInfo,
|
||||
}),
|
||||
numberOfChannels: this.elementaryStream.info.numberOfChannels,
|
||||
sampleRate: this.elementaryStream.info.sampleRate,
|
||||
};
|
||||
}
|
||||
|
||||
override allPacketsAreKeyPackets(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override getReorderSize(): number {
|
||||
return 1; // No reordering, since no B-frames because goated
|
||||
}
|
||||
}
|
||||
|
||||
const maybeInsertReferencePacket = (elementaryStream: ElementaryStream, pesPacketHeader: PesPacketHeader) => {
|
||||
const referencePesPackets = elementaryStream.referencePesPackets;
|
||||
|
||||
const index = binarySearchLessOrEqual(
|
||||
referencePesPackets,
|
||||
pesPacketHeader.sectionStartPos,
|
||||
x => x.sectionStartPos,
|
||||
);
|
||||
if (index >= 0) {
|
||||
// Since pts and file position don't necessarily have a monotonic relationship (since pts can go crazy),
|
||||
// let's see if inserting at the given index would violate the pts order. If so, return.
|
||||
const entry = referencePesPackets[index]!;
|
||||
if (pesPacketHeader.pts <= entry.pts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const minByteDistance = elementaryStream.demuxer.minReferencePointByteDistance;
|
||||
|
||||
if (pesPacketHeader.sectionStartPos - entry.sectionStartPos < minByteDistance) {
|
||||
// Too close
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index < referencePesPackets.length - 1) {
|
||||
const nextEntry = referencePesPackets[index + 1]!;
|
||||
if (nextEntry.pts < pesPacketHeader.pts) {
|
||||
// Out of order
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextEntry.sectionStartPos - pesPacketHeader.sectionStartPos < minByteDistance) {
|
||||
// Too close
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
referencePesPackets.splice(index + 1, 0, pesPacketHeader);
|
||||
return true;
|
||||
};
|
||||
|
||||
const markNextPacket = async (context: PacketReadingContext) => {
|
||||
assert(!context.suppliedPacket);
|
||||
|
||||
const elementaryStream = context.elementaryStream;
|
||||
|
||||
if (elementaryStream.info.type === 'video') {
|
||||
const codec = elementaryStream.info.codec;
|
||||
const CHUNK_SIZE = 1024;
|
||||
|
||||
if (codec !== 'avc' && codec !== 'hevc') {
|
||||
@@ -1528,53 +1611,8 @@ class MpegTsVideoTrackBacking extends MpegTsTrackBacking implements InputVideoTr
|
||||
context.seekTo(packetStartPos);
|
||||
return context.supplyPacket(packetLength, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTrackBacking {
|
||||
override elementaryStream: ElementaryAudioStream;
|
||||
|
||||
constructor(elementaryStream: ElementaryAudioStream) {
|
||||
super(elementaryStream);
|
||||
this.elementaryStream = elementaryStream;
|
||||
}
|
||||
|
||||
override getCodec(): AudioCodec {
|
||||
return this.elementaryStream.info.codec;
|
||||
}
|
||||
|
||||
getNumberOfChannels() {
|
||||
return this.elementaryStream.info.numberOfChannels;
|
||||
}
|
||||
|
||||
getSampleRate() {
|
||||
return this.elementaryStream.info.sampleRate;
|
||||
}
|
||||
|
||||
async getDecoderConfig(): Promise<AudioDecoderConfig> {
|
||||
return {
|
||||
codec: extractAudioCodecString({
|
||||
codec: this.elementaryStream.info.codec,
|
||||
codecDescription: null,
|
||||
aacCodecInfo: this.elementaryStream.info.aacCodecInfo,
|
||||
}),
|
||||
numberOfChannels: this.elementaryStream.info.numberOfChannels,
|
||||
sampleRate: this.elementaryStream.info.sampleRate,
|
||||
};
|
||||
}
|
||||
|
||||
override allPacketsAreKeyPackets(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override getReorderSize(): number {
|
||||
return 1; // No reordering, since no B-frames because goated
|
||||
}
|
||||
|
||||
override async markNextPacket(context: PacketReadingContext): Promise<void> {
|
||||
assert(!context.suppliedPacket);
|
||||
|
||||
const codec = this.elementaryStream.info.codec;
|
||||
} else {
|
||||
const codec = elementaryStream.info.codec;
|
||||
const CHUNK_SIZE = 128;
|
||||
|
||||
while (true) {
|
||||
@@ -1612,7 +1650,7 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr
|
||||
|
||||
return context.supplyPacket(
|
||||
remaining,
|
||||
Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / this.elementaryStream.info.sampleRate),
|
||||
Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / elementaryStream.info.sampleRate),
|
||||
);
|
||||
} else {
|
||||
context.seekTo(possibleHeaderStartPos + 1);
|
||||
@@ -1643,7 +1681,7 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr
|
||||
if (remaining instanceof Promise) remaining = await remaining;
|
||||
|
||||
const duration = result.header.audioSamplesInFrame * TIMESCALE
|
||||
/ this.elementaryStream.info.sampleRate;
|
||||
/ elementaryStream.info.sampleRate;
|
||||
return context.supplyPacket(remaining, Math.round(duration));
|
||||
} else {
|
||||
context.seekTo(possibleHeaderStartPos + 1);
|
||||
@@ -1658,7 +1696,7 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type SuppliedPacket = {
|
||||
pts: number;
|
||||
@@ -1670,11 +1708,10 @@ type SuppliedPacket = {
|
||||
|
||||
/** Stateful context used to extract exact encoded packets from the underlying data stream. */
|
||||
class PacketReadingContext {
|
||||
backing: MpegTsTrackBacking;
|
||||
elementaryStream: ElementaryStream;
|
||||
pid: number;
|
||||
demuxer: MpegTsDemuxer;
|
||||
startingPesPacket: PesPacket;
|
||||
uncapped: boolean;
|
||||
|
||||
currentPos = 0; // Relative to the data in startingPesPacket
|
||||
pesPackets: PesPacket[] = [];
|
||||
@@ -1685,16 +1722,15 @@ class PacketReadingContext {
|
||||
|
||||
suppliedPacket: SuppliedPacket | null = null;
|
||||
|
||||
constructor(backing: MpegTsTrackBacking, startingPesPacket: PesPacket, uncapped: boolean) {
|
||||
this.backing = backing;
|
||||
this.pid = backing.elementaryStream.pid;
|
||||
this.demuxer = backing.elementaryStream.demuxer;
|
||||
constructor(elementaryStream: ElementaryStream, startingPesPacket: PesPacket) {
|
||||
this.elementaryStream = elementaryStream;
|
||||
this.pid = elementaryStream.pid;
|
||||
this.demuxer = elementaryStream.demuxer;
|
||||
this.startingPesPacket = startingPesPacket;
|
||||
this.uncapped = uncapped;
|
||||
}
|
||||
|
||||
clone() {
|
||||
const clone = new PacketReadingContext(this.backing, this.startingPesPacket, true); // Close isn't capped
|
||||
const clone = new PacketReadingContext(this.elementaryStream, this.startingPesPacket);
|
||||
clone.currentPos = this.currentPos;
|
||||
clone.pesPackets = [...this.pesPackets];
|
||||
clone.currentPesPacketIndex = this.currentPesPacketIndex;
|
||||
@@ -1741,23 +1777,20 @@ class PacketReadingContext {
|
||||
}
|
||||
|
||||
if (packetHeader.pid === this.pid) {
|
||||
break;
|
||||
const nextSection = await this.demuxer.readSection(currentPos, true);
|
||||
if (!nextSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPesPacket = readPesPacket(nextSection);
|
||||
if (nextPesPacket) {
|
||||
pesPacket = nextPesPacket;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentPos += this.demuxer.packetStride;
|
||||
}
|
||||
|
||||
const nextSection = await this.demuxer.readSection(currentPos, true);
|
||||
if (!nextSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPesPacket = readPesPacket(nextSection);
|
||||
if (!nextPesPacket) {
|
||||
throw new Error(MISSING_PES_PACKET_ERROR);
|
||||
}
|
||||
|
||||
pesPacket = nextPesPacket;
|
||||
}
|
||||
|
||||
this.pesPackets.push(pesPacket);
|
||||
@@ -1868,14 +1901,8 @@ class PacketReadingContext {
|
||||
/** Supplies the context with a new encoded packet, beginning at the current position. */
|
||||
supplyPacket(packetLength: number, intrinsicDuration: number) {
|
||||
const currentPesPacket = this.getCurrentPesPacket();
|
||||
if (!this.uncapped && currentPesPacket !== this.startingPesPacket) {
|
||||
// The packet is "outside" of the valid region, the valid region is any packet starting in the starting
|
||||
// section
|
||||
this.suppliedPacket = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.backing.maybeInsertReferencePacket(currentPesPacket);
|
||||
maybeInsertReferencePacket(this.elementaryStream, currentPesPacket);
|
||||
|
||||
const pts = this.nextPts;
|
||||
this.nextPts += intrinsicDuration;
|
||||
@@ -1888,8 +1915,8 @@ class PacketReadingContext {
|
||||
|
||||
let randomAccessIndicator = currentPesPacket.randomAccessIndicator;
|
||||
|
||||
assert(this.backing.elementaryStream.firstSection);
|
||||
if (currentPesPacket.sectionStartPos === this.backing.elementaryStream.firstSection.startPos) {
|
||||
assert(this.elementaryStream.firstSection);
|
||||
if (currentPesPacket.sectionStartPos === this.elementaryStream.firstSection.startPos) {
|
||||
randomAccessIndicator = 1; // Force the first PES packet to behave like a key packet always
|
||||
}
|
||||
|
||||
@@ -1980,7 +2007,7 @@ class PacketBuffer {
|
||||
// Small optimization: there was already a supplied packet in the context, so let's first use that one
|
||||
suppliedPacket = this.context.suppliedPacket;
|
||||
} else {
|
||||
await this.backing.markNextPacket(this.context);
|
||||
await markNextPacket(this.context);
|
||||
suppliedPacket = this.context.suppliedPacket;
|
||||
}
|
||||
this.context.suppliedPacket = null;
|
||||
|
||||
+11
-3
@@ -343,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.
|
||||
@@ -403,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
|
||||
@@ -414,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,
|
||||
});
|
||||
|
||||
@@ -425,6 +425,8 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
packetIndex: number,
|
||||
options: PacketRetrievalOptions,
|
||||
): Promise<EncodedPacket | null> {
|
||||
assert(packetIndex >= 0);
|
||||
|
||||
assert(this.demuxer.audioInfo);
|
||||
const startOffset = packetIndex * PACKET_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes;
|
||||
if (startOffset >= this.demuxer.dataSize) {
|
||||
@@ -465,7 +467,7 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
|
||||
this.demuxer.lastKnownPacketIndex = Math.max(
|
||||
packetIndex,
|
||||
timestamp,
|
||||
this.demuxer.lastKnownPacketIndex,
|
||||
);
|
||||
|
||||
return new EncodedPacket(
|
||||
@@ -489,6 +491,9 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
timestamp * this.demuxer.audioInfo.sampleRate / PACKET_SIZE_IN_FRAMES,
|
||||
(this.demuxer.dataSize - 1) / (PACKET_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes),
|
||||
));
|
||||
if (packetIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packet = await this.getPacketAtIndex(packetIndex, options);
|
||||
if (packet) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user