mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Add fastStart: 'reserve' option (closes #119)
This commit is contained in:
+3
-2
@@ -46,6 +46,7 @@
|
||||
format = new Mediabunny.WavOutputFormat();
|
||||
format = new Mediabunny.MkvOutputFormat();
|
||||
format = new Mediabunny.MovOutputFormat();
|
||||
format = new Mediabunny.Mp4OutputFormat({ fastStart: 'reserve' });
|
||||
let target = new Mediabunny.BufferTarget();
|
||||
|
||||
/*
|
||||
@@ -127,8 +128,8 @@
|
||||
});
|
||||
let subtitleSource = new Mediabunny.TextSubtitleSource('webvtt');
|
||||
|
||||
output.addVideoTrack(videoSource, { languageCode: 'eng', name: 'Mononoké' });
|
||||
output.addAudioTrack(audioSource, { name: 'Yooo' });
|
||||
output.addVideoTrack(videoSource, { languageCode: 'eng', name: 'Mononoké', maximumPacketCount: 100 });
|
||||
output.addAudioTrack(audioSource, { name: 'Yooo', maximumPacketCount: 1000 });
|
||||
//output.addSubtitleTrack(subtitleSource);
|
||||
|
||||
output.start();
|
||||
|
||||
@@ -62,7 +62,7 @@ const output = new Output({
|
||||
The following options are available:
|
||||
```ts
|
||||
type IsobmffOutputFormatOptions = {
|
||||
fastStart?: false | 'in-memory' | 'fragmented';
|
||||
fastStart?: false | 'in-memory' | 'reserve' | 'fragmented';
|
||||
minimumFragmentDuration?: number;
|
||||
|
||||
onFtyp?: (data: Uint8Array, position: number) => unknown;
|
||||
@@ -80,6 +80,8 @@ type IsobmffOutputFormatOptions = {
|
||||
::: info
|
||||
This option ensures [append-only writing](#append-only-writing), although all the writing happens in bulk, at the end.
|
||||
:::
|
||||
- `'reserve'`\
|
||||
Produces a file with Fast Start by reserving space at the start of the file into which the metadata will be written later. This requires knowledge about the expected length of the file beforehand. When using this option, you must set the [`maximumPacketCount`](../api/BaseTrackMetadata#maximumpacketcount) field in the track metadata for all tracks.
|
||||
- `'fragmented'`\
|
||||
Produces a _fragmented MP4 (fMP4)_ file, evenly placing sample metadata throughout the file by grouping it into "fragments" (short sections of media), while placing general metadata at the beginning of the file. Fragmented files are ideal in streaming contexts, as each fragment can be played individually without requiring knowledge of the other fragments. Furthermore, they remain lightweight to create no matter how large the file becomes, as they don't require media to be kept in memory for very long. However, fragmented files are not as widely and wholly supported as regular MP4 files, and some players don't provide seeking functionality for them.
|
||||
::: info
|
||||
|
||||
+100
-18
@@ -169,29 +169,72 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
|
||||
|
||||
// Helper to process {@link} tags in JSDoc comments
|
||||
const processLinkTags = (text: string, currentTypeName?: string): string => {
|
||||
// Replace {@link TypeName} with [TypeName](./TypeName.md) if TypeName is exported
|
||||
// or just TypeName if not exported
|
||||
// If TypeName is the current type, just use code formatting without link
|
||||
return text.replace(/\{@link\s+([^}]+)\}/g, (_, typeName) => {
|
||||
const cleanTypeName = typeName.trim();
|
||||
if (cleanTypeName === currentTypeName) {
|
||||
return `\`${cleanTypeName}\``;
|
||||
// Updated regex to handle member links and optional link text, e.g., {@link Type.member | text}
|
||||
return text.replace(/\{@link\s+([^}|]+)(?:\s*\|\s*([^}]+))?\}/g, (_, target, linkText) => {
|
||||
const cleanTarget = target.trim();
|
||||
|
||||
// Split into type and member parts
|
||||
const parts = cleanTarget.split('.');
|
||||
const typeName = parts[0];
|
||||
const memberName = parts.length > 1 ? parts[1] : undefined;
|
||||
|
||||
let displayText: string;
|
||||
if (linkText) {
|
||||
// If custom link text is provided, always use it.
|
||||
displayText = linkText.trim();
|
||||
} else if (memberName) {
|
||||
// If it's a member link, default the text to just the member name.
|
||||
displayText = `\`${memberName}\``;
|
||||
} else {
|
||||
// Otherwise, it's a type link, so use the full type name.
|
||||
displayText = `\`${cleanTarget}\``;
|
||||
}
|
||||
if (exportedTypes.has(cleanTypeName)) {
|
||||
return `[\`${cleanTypeName}\`](./${cleanTypeName}.md)`;
|
||||
|
||||
// Check if the base type is a known exported type
|
||||
if (exportedTypes.has(typeName)) {
|
||||
let linkUrl = '';
|
||||
|
||||
if (memberName) {
|
||||
// It's a link to a member (property or method)
|
||||
const anchor = memberName.toLowerCase();
|
||||
|
||||
if (typeName === currentTypeName) {
|
||||
// Link to an anchor on the same page
|
||||
linkUrl = `#${anchor}`;
|
||||
} else {
|
||||
// Link to another page's anchor
|
||||
linkUrl = `./${typeName}.md#${anchor}`;
|
||||
}
|
||||
return `\`${cleanTypeName}\``;
|
||||
} else {
|
||||
// It's a link to a type
|
||||
if (typeName === currentTypeName) {
|
||||
// Don't link to the current page, just format it
|
||||
return `\`${cleanTarget}\``;
|
||||
}
|
||||
linkUrl = `./${typeName}.md`;
|
||||
}
|
||||
return `[${displayText}](${linkUrl})`;
|
||||
}
|
||||
|
||||
// Fallback for unknown types: just format as code
|
||||
return `\`${cleanTarget}\``;
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to extract linked types from {@link} tags in text
|
||||
const extractLinkedTypes = (text: string): string[] => {
|
||||
if (!text) return [];
|
||||
const linkMatches = text.match(/\{@link\s+([^}]+)\}/g) || [];
|
||||
return linkMatches.map((match) => {
|
||||
const typeName = match.replace(/\{@link\s+([^}]+)\}/, '$1').trim();
|
||||
return typeName;
|
||||
});
|
||||
const linkedTypes: string[] = [];
|
||||
// Use a regex to find all link targets
|
||||
const regex = /\{@link\s+([^}|]+)/g;
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const target = match[1]!.trim();
|
||||
// Return only the base type name (the part before the first dot)
|
||||
const typeName = target.split('.')[0];
|
||||
linkedTypes.push(typeName!);
|
||||
}
|
||||
return linkedTypes;
|
||||
};
|
||||
|
||||
// Helper to format references with proper "and" and period
|
||||
@@ -1206,9 +1249,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
|
||||
let desc = '';
|
||||
const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
|
||||
if (propDeclaration) {
|
||||
const jsDoc = ts.getJSDocCommentsAndTags(propDeclaration)[0];
|
||||
if (jsDoc && ts.isJSDoc(jsDoc) && typeof jsDoc.comment === 'string') {
|
||||
desc = processLinkTags(jsDoc.comment.trim(), className);
|
||||
const rawDesc = getFullJSDocDescription(propDeclaration);
|
||||
if (rawDesc) {
|
||||
desc = processLinkTags(rawDesc, className);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1620,6 +1663,45 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to get the full description text from a JSDoc comment, handling inline tags.
|
||||
const getFullJSDocDescription = (node: ts.Node): string => {
|
||||
const jsDoc = ts.getJSDocCommentsAndTags(node)[0];
|
||||
if (!jsDoc || !ts.isJSDoc(jsDoc)) return '';
|
||||
|
||||
// If it's a simple string, just return it.
|
||||
if (typeof jsDoc.comment === 'string') {
|
||||
return jsDoc.comment.trim();
|
||||
}
|
||||
|
||||
// If it's a structured comment (with inline tags), get the raw text.
|
||||
const sourceFile = node.getSourceFile();
|
||||
const sourceText = sourceFile.getFullText();
|
||||
const start = jsDoc.getStart();
|
||||
const end = jsDoc.getEnd();
|
||||
const rawJsDoc = sourceText.substring(start, end);
|
||||
|
||||
// Extract the content between /** and */
|
||||
const match = rawJsDoc.match(/\/\*\*(.*?)\*\//s);
|
||||
if (match && match[1]) {
|
||||
const content = match[1]
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s*\*\s?/, '')) // Remove leading * and spaces
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
// Filter out @-tags (like @param, @returns) to keep only the main description
|
||||
const lines = content.split('\n');
|
||||
const descLines = [];
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('@')) break; // Stop at the first @-tag
|
||||
descLines.push(line);
|
||||
}
|
||||
return descLines.join('\n').trim();
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
|
||||
@@ -330,17 +330,17 @@ export const ftyp = (details: {
|
||||
/** Movie Sample Data Box. Contains the actual frames/samples of the media. */
|
||||
export const mdat = (reserveLargeSize: boolean): Box => ({ type: 'mdat', largeSize: reserveLargeSize });
|
||||
|
||||
/** Free Space Box: A box that designates unused space in the movie data file. */
|
||||
export const free = (size: number): Box => ({ type: 'free', size });
|
||||
|
||||
/**
|
||||
* Movie Box: Used to specify the information that defines a movie - that is, the information that allows
|
||||
* an application to interpret the sample data that is stored elsewhere.
|
||||
*/
|
||||
export const moov = (
|
||||
muxer: IsobmffMuxer,
|
||||
fragmented = false,
|
||||
) => box('moov', undefined, [
|
||||
export const moov = (muxer: IsobmffMuxer) => box('moov', undefined, [
|
||||
mvhd(muxer.creationTime, muxer.trackDatas),
|
||||
...muxer.trackDatas.map(x => trak(x, muxer.creationTime)),
|
||||
fragmented ? mvex(muxer.trackDatas) : null,
|
||||
muxer.isFragmented ? mvex(muxer.trackDatas) : null,
|
||||
udta(muxer),
|
||||
]);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Box, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff-boxes';
|
||||
import { Box, free, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff-boxes';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
|
||||
import { BufferTargetWriter, Writer } from '../writer';
|
||||
@@ -142,7 +142,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
private writer: Writer;
|
||||
private boxWriter: IsobmffBoxWriter;
|
||||
private fastStart: NonNullable<IsobmffOutputFormatOptions['fastStart']>;
|
||||
private isFragmented: boolean;
|
||||
isFragmented: boolean;
|
||||
|
||||
isQuickTime: boolean;
|
||||
|
||||
@@ -151,6 +151,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
private auxBoxWriter = new IsobmffBoxWriter(this.auxWriter);
|
||||
|
||||
private mdat: Box | null = null;
|
||||
private ftypSize: number | null = null;
|
||||
|
||||
trackDatas: IsobmffTrackData[] = [];
|
||||
private allTracksKnown = promiseWithResolvers();
|
||||
@@ -208,8 +209,22 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
this.ftypSize = this.writer.getPos();
|
||||
|
||||
if (this.fastStart === 'in-memory') {
|
||||
this.mdat = mdat(false);
|
||||
// We're write at finalization
|
||||
} else if (this.fastStart === 'reserve') {
|
||||
// Validate that all tracks have set maximumPacketCount
|
||||
for (const track of this.output._tracks) {
|
||||
if (track.metadata.maximumPacketCount === undefined) {
|
||||
throw new Error(
|
||||
'All tracks must specify maximumPacketCount in their metadata when using'
|
||||
+ ' fastStart: \'reserve\'.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We'll start writing once we know all tracks
|
||||
} else if (this.isFragmented) {
|
||||
// We write the moov box once we write out the first fragment to make sure we get the decoder configs
|
||||
} else {
|
||||
@@ -830,6 +845,8 @@ export class IsobmffMuxer extends Muxer {
|
||||
if (this.isFragmented) {
|
||||
trackData.sampleQueue.push(sample);
|
||||
await this.interleaveSamples();
|
||||
} else if (this.fastStart === 'reserve') {
|
||||
await this.registerSampleFastStartReserve(trackData, sample);
|
||||
} else {
|
||||
await this.addSampleToTrack(trackData, sample);
|
||||
}
|
||||
@@ -838,6 +855,18 @@ export class IsobmffMuxer extends Muxer {
|
||||
private async addSampleToTrack(trackData: IsobmffTrackData, sample: Sample) {
|
||||
if (!this.isFragmented) {
|
||||
trackData.samples.push(sample);
|
||||
|
||||
if (this.fastStart === 'reserve') {
|
||||
const maximumPacketCount = trackData.track.metadata.maximumPacketCount;
|
||||
assert(maximumPacketCount !== undefined);
|
||||
|
||||
if (trackData.samples.length > maximumPacketCount) {
|
||||
throw new Error(
|
||||
`Track #${trackData.track.id} has already reached the maximum packet count`
|
||||
+ ` (${maximumPacketCount}). Either add less packets or increase the maximum packet count.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let beginNewChunk = false;
|
||||
@@ -946,11 +975,9 @@ export class IsobmffMuxer extends Muxer {
|
||||
private async interleaveSamples(isFinalCall = false) {
|
||||
assert(this.isFragmented);
|
||||
|
||||
if (!isFinalCall) {
|
||||
if (!this.allTracksAreKnown()) {
|
||||
if (!isFinalCall && !this.allTracksAreKnown()) {
|
||||
return; // We can't interleave yet as we don't yet know how many tracks we'll truly have
|
||||
}
|
||||
}
|
||||
|
||||
outer:
|
||||
while (true) {
|
||||
@@ -988,7 +1015,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
// Write the moov box now that we have all decoder configs
|
||||
const movieBox = moov(this, true);
|
||||
const movieBox = moov(this);
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
|
||||
if (this.format._options.onMoov) {
|
||||
@@ -1077,6 +1104,72 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
private async registerSampleFastStartReserve(trackData: IsobmffTrackData, sample: Sample) {
|
||||
if (this.allTracksAreKnown()) {
|
||||
if (!this.mdat) {
|
||||
// We finally know all tracks, let's reserve space for the moov box
|
||||
const moovBox = moov(this);
|
||||
const moovSize = this.boxWriter.measureBox(moovBox);
|
||||
|
||||
const reservedSize = moovSize
|
||||
+ this.computeSampleTableSizeUpperBound()
|
||||
+ 4096; // Just a little extra headroom
|
||||
|
||||
assert(this.ftypSize !== null);
|
||||
this.writer.seek(this.ftypSize + reservedSize);
|
||||
|
||||
if (this.format._options.onMdat) {
|
||||
this.writer.startTrackingWrites();
|
||||
}
|
||||
|
||||
this.mdat = mdat(true);
|
||||
this.boxWriter.writeBox(this.mdat);
|
||||
|
||||
// Now write everything that was queued
|
||||
for (const trackData of this.trackDatas) {
|
||||
for (const sample of trackData.sampleQueue) {
|
||||
await this.addSampleToTrack(trackData, sample);
|
||||
}
|
||||
trackData.sampleQueue.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
await this.addSampleToTrack(trackData, sample);
|
||||
} else {
|
||||
// Queue it for when we know all tracks
|
||||
trackData.sampleQueue.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
private computeSampleTableSizeUpperBound() {
|
||||
assert(this.fastStart === 'reserve');
|
||||
|
||||
let upperBound = 0;
|
||||
|
||||
for (const trackData of this.trackDatas) {
|
||||
const n = trackData.track.metadata.maximumPacketCount;
|
||||
assert(n !== undefined); // We validated this earlier
|
||||
|
||||
// Given the max allowed packet count, compute the space they'll take up in the Sample Table Box, assuming
|
||||
// the worst case for each individual box:
|
||||
|
||||
// stts box - since it is compactly coded, the maximum length of this table will be 2/3n
|
||||
upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
|
||||
// stss box - 1 entry per sample
|
||||
upperBound += 4 * n;
|
||||
// ctts box - since it is compactly coded, the maximum length of this table will be 2/3n
|
||||
upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
|
||||
// stsc box - since it is compactly coded, the maximum length of this table will be 2/3n
|
||||
upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n);
|
||||
// stsz box - 1 entry per sample
|
||||
upperBound += 4 * n;
|
||||
// co64 box - we assume 1 sample per chunk and 64-bit chunk offsets (co64 instead of stco)
|
||||
upperBound += 8 * n;
|
||||
}
|
||||
|
||||
return upperBound;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
override async onTrackClose(track: OutputTrack) {
|
||||
const release = await this.mutex.acquire();
|
||||
@@ -1128,7 +1221,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
if (this.fastStart === 'in-memory') {
|
||||
assert(this.mdat);
|
||||
this.mdat = mdat(false);
|
||||
let mdatSize: number;
|
||||
|
||||
// We know how many chunks there are, but computing the chunk positions requires an iterative approach:
|
||||
@@ -1214,13 +1307,29 @@ export class IsobmffMuxer extends Muxer {
|
||||
this.format._options.onMdat(data, start);
|
||||
}
|
||||
|
||||
const movieBox = moov(this);
|
||||
|
||||
if (this.fastStart === 'reserve') {
|
||||
assert(this.ftypSize !== null);
|
||||
this.writer.seek(this.ftypSize);
|
||||
|
||||
if (this.format._options.onMoov) {
|
||||
this.writer.startTrackingWrites();
|
||||
}
|
||||
|
||||
const movieBox = moov(this);
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
|
||||
// Fill the remaining space with a free box. If there are less than 8 bytes left, sucks I guess
|
||||
const remainingSpace = this.boxWriter.offsets.get(this.mdat)! - this.writer.getPos();
|
||||
this.boxWriter.writeBox(free(remainingSpace));
|
||||
} else {
|
||||
if (this.format._options.onMoov) {
|
||||
this.writer.startTrackingWrites();
|
||||
}
|
||||
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
}
|
||||
|
||||
if (this.format._options.onMoov) {
|
||||
const { data, start } = this.writer.stopTrackingWrites();
|
||||
this.format._options.onMoov(data, start);
|
||||
|
||||
@@ -854,11 +854,9 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
private async interleaveChunks(isFinalCall = false) {
|
||||
if (!isFinalCall) {
|
||||
if (!this.allTracksAreKnown()) {
|
||||
if (!isFinalCall && !this.allTracksAreKnown()) {
|
||||
return; // We can't interleave yet as we don't yet know how many tracks we'll truly have
|
||||
}
|
||||
}
|
||||
|
||||
outer:
|
||||
while (true) {
|
||||
|
||||
+13
-3
@@ -115,6 +115,11 @@ export type IsobmffOutputFormatOptions = {
|
||||
* finalized. This produces a high-quality and compact output at the cost of a more expensive finalization step and
|
||||
* higher memory requirements. Data will be written monotonically (in order) when this option is set.
|
||||
*
|
||||
* Use `'reserve'` to reserve space at the start of the file into which the metadata will be written later. This
|
||||
* produces a file with Fast Start but requires knowledge about the expected length of the file beforehand. When
|
||||
* using this option, you must set the {@link BaseTrackMetadata.maximumPacketCount} field in the track metadata
|
||||
* for all tracks.
|
||||
*
|
||||
* Use `'fragmented'` to place metadata at the start of the file by creating a fragmented file (fMP4). In a
|
||||
* fragmented file, chunks of media and their metadata are written to the file in "fragments", eliminating the need
|
||||
* to put all metadata in one place. Fragmented files are useful for streaming contexts, as each fragment can be
|
||||
@@ -126,7 +131,7 @@ export type IsobmffOutputFormatOptions = {
|
||||
* When this field is not defined, either `false` or `'in-memory'` will be used, automatically determined based on
|
||||
* the type of output target used.
|
||||
*/
|
||||
fastStart?: false | 'in-memory' | 'fragmented';
|
||||
fastStart?: false | 'in-memory' | 'reserve' | 'fragmented';
|
||||
|
||||
/**
|
||||
* When using `fastStart: 'fragmented'`, this field controls the minimum duration of each fragment, in seconds.
|
||||
@@ -184,8 +189,13 @@ export abstract class IsobmffOutputFormat extends OutputFormat {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError('options must be an object.');
|
||||
}
|
||||
if (options.fastStart !== undefined && ![false, 'in-memory', 'fragmented'].includes(options.fastStart)) {
|
||||
throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');
|
||||
if (
|
||||
options.fastStart !== undefined
|
||||
&& ![false, 'in-memory', 'reserve', 'fragmented'].includes(options.fastStart)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'options.fastStart, when provided, must be false, \'in-memory\', \'reserve\', or \'fragmented\'.',
|
||||
);
|
||||
}
|
||||
if (
|
||||
options.minimumFragmentDuration !== undefined
|
||||
|
||||
@@ -74,6 +74,21 @@ export type BaseTrackMetadata = {
|
||||
languageCode?: string;
|
||||
/** A user-defined name for this track, like "English" or "Director Commentary". */
|
||||
name?: string;
|
||||
/**
|
||||
* The maximum amount of encoded packets that will be added to this track. Setting this field provides the muxer
|
||||
* with an additional signal that it can use to preallocate space in the file.
|
||||
*
|
||||
* When this field is set, it is an error to provide more packets than whatever this field specifies.
|
||||
*
|
||||
* Predicting the maximum packet count requires considering both the maximum duration as well as the codec.
|
||||
* - For video codecs, you can assume one packet per frame.
|
||||
* - For audio codecs, there is one packet for each "audio chunk", the duration of which depends on the codec. For
|
||||
* simplicity, you can assume each packet is roughly 10 ms or 512 samples long, whichever is shorter.
|
||||
* - For subtitles, assume each cue and each gap in the subtitles adds a packet.
|
||||
*
|
||||
* If you're not fully sure, make sure to add a buffer of around 33% to make sure you stay below the maximum.
|
||||
*/
|
||||
maximumPacketCount?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -114,6 +129,12 @@ const validateBaseTrackMetadata = (metadata: BaseTrackMetadata) => {
|
||||
if (metadata.name !== undefined && typeof metadata.name !== 'string') {
|
||||
throw new TypeError('metadata.name, when provided, must be a string.');
|
||||
}
|
||||
if (
|
||||
metadata.maximumPacketCount !== undefined
|
||||
&& (!Number.isInteger(metadata.maximumPacketCount) || metadata.maximumPacketCount < 0)
|
||||
) {
|
||||
throw new TypeError('metadata.maximumPacketCount, when provided, must be a non-negative integer.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user