From 441eea38b97f4be1c027e53c5803bdf7b7e5e6aa Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 24 May 2026 15:48:01 +0200 Subject: [PATCH] CustomSource -> StreamSource --- docs/guide/reading-media-files.md | 18 +++-- scripts/generate-api-docs.ts | 101 ++++++++++++++++++----------- src/index.ts | 4 ++ src/source.ts | 52 +++++++++++---- test/node/mpeg-ts-demuxing.test.ts | 4 +- 5 files changed, 119 insertions(+), 60 deletions(-) diff --git a/docs/guide/reading-media-files.md b/docs/guide/reading-media-files.md index 3c44b80..811b52c 100644 --- a/docs/guide/reading-media-files.md +++ b/docs/guide/reading-media-files.md @@ -696,18 +696,18 @@ type FilePathSourceOptions = { When using this source, make sure to manually [dispose of the Input](#disposing-inputs) when you are done with it to properly close the internal file handle held by this source. ::: -### `StreamSource` +### `CustomSource` This is a general-purpose input source you can use to read data from anywhere. -For example, here we're reading a file from disk using the Node.js file system (although you should use [`FilePathSource`](#filepathsource) for that): +For example, here we're reading a file from disk using the Node.js file system (although you should use the existing [`FilePathSource`](#filepathsource) for that): ```ts -import { StreamSource } from 'mediabunny'; +import { CustomSource } from 'mediabunny'; import { open } from 'node:fs/promises'; const fileHandle = await open('bigbuckbunny.mp4', 'r'); -const source = new StreamSource({ +const source = new CustomSource({ read: async (start, end) => { const buffer = Buffer.alloc(end - start); await fileHandle.read(buffer, 0, end - start, start); @@ -720,9 +720,9 @@ const source = new StreamSource({ }); ``` -The options of `StreamSource` have the following type: +The options of `CustomSource` have the following type: ```ts -type StreamSourceOptions = { +type CustomSourceOptions = { getSize: () => MaybePromise; read: (start: number, end: number) => MaybePromise>; dispose?: () => unknown; @@ -736,7 +736,7 @@ type MaybePromise = T | Promise; - `getSize`\ Called when the size of the entire file is requested. Must return or resolve to the size in bytes. This function is guaranteed to be called before `read`. - `read`\ - Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes. + Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes. You are guaranteed that `0 <= start < end < fileSize`. - `dispose`\ Called when the `Input` driven by this source is disposed. - `maxCacheSize`\ @@ -747,6 +747,10 @@ type MaybePromise = T | Promise; - `'fileSystem'`: File system-optimized prefetching: a small amount of data is prefetched bidirectionally, aligned with page boundaries. - `'network'`: Network-optimized prefetching, or more generally, prefetching optimized for any high-latency environment: tries to minimize the amount of read calls and aggressively prefetches data when sequential access patterns are detected. +::: info +`CustomSource` was previously known as `StreamSource` and is still available under that alias, but usage of `StreamSource` is deprecated. +::: + ### `ReadableStreamSource` This is a source backed by a `ReadableStream` of `Uint8Array`, representing an append-only byte stream of unknown length. This is the source to use for incrementally streaming in input files that are still being constructed and whose size we don't yet know. You could also use it to stream in existing files, but other sources (such as [`BlobSource`](#blobsource) or [`FilePathSource`](#filepathsource)) are recommended instead because they offer random access. diff --git a/scripts/generate-api-docs.ts b/scripts/generate-api-docs.ts index 6a4738c..a0f4347 100644 --- a/scripts/generate-api-docs.ts +++ b/scripts/generate-api-docs.ts @@ -83,12 +83,12 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; if (!declaration) return; - // Collect classes, interfaces, types, enums, variables (only if @public and not @deprecated) + // Collect classes, interfaces, types, enums, variables (only if @public) if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration) || ts.isVariableDeclaration(declaration)) { const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); - if (hasPublicTag && !hasDeprecatedTag(declaration)) { + if (hasPublicTag) { exportedTypes.add(exportSymbol.getName()); } } @@ -98,9 +98,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) const aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol); const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]; if (aliasedDeclaration) { - // Check if the aliased symbol has @public tag and is not deprecated + // Check if the aliased symbol has @public tag const hasPublicTag = ts.getJSDocTags(aliasedDeclaration).some(tag => tag.tagName.text === 'public'); - if (hasPublicTag && !hasDeprecatedTag(aliasedDeclaration)) { + if (hasPublicTag) { exportedTypes.add(exportSymbol.getName()); } @@ -139,10 +139,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) } } } - // Otherwise, add any symbol with @public tag and not @deprecated (we'll filter by type later) + // Otherwise, add any symbol with @public tag (we'll filter by type later) else { const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); - if (hasPublicTag && !hasDeprecatedTag(declaration)) { + if (hasPublicTag) { symbols.push(exportSymbol); } } @@ -379,6 +379,34 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) }); }; + // Helper to build a "> **Deprecated.** ..." notice from a node's @deprecated JSDoc tag. + // Returns an empty string if the node has no @deprecated tag. + const getDeprecationNotice = (node: ts.Node, currentTypeName?: string): string => { + const tag = ts.getJSDocTags(node).find(t => t.tagName.text === 'deprecated'); + if (!tag) { + return ''; + } + let text = ''; + if (tag.comment) { + if (typeof tag.comment === 'string') { + text = processLinkTags(tag.comment.trim(), currentTypeName); + } else { + // comment is a NodeArray of JSDocComment elements (text + inline tags) + const raw = tag.comment.map((part) => { + if (ts.isJSDocLinkLike(part)) { + const linkName = part.name?.getText() ?? ''; + const linkText = part.text?.trim() ?? ''; + // Reconstruct as {@link Name text} + return `{@link ${linkName}${linkText ? ' ' + linkText : ''}}`; + } + return part.text ?? ''; + }).join(''); + text = processLinkTags(raw.trim(), currentTypeName); + } + } + return text ? `> **Deprecated.** ${text}\n\n` : '> **Deprecated.**\n\n'; + }; + // Helper to extract linked types from {@link} tags in text const extractLinkedTypes = (text: string): string[] => { if (!text) return []; @@ -627,10 +655,11 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) const nodeKind = ts.SyntaxKind[declaration.kind]; const symbolName = (declaration as any).name?.getText() || exportSymbol.getName(); - // Only process symbols with @public tag, and skip deprecated ones entirely + // Only process symbols with @public tag const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); - if (!hasPublicTag) return; - if (hasDeprecatedTag(declaration)) return; + if (!hasPublicTag) { + return; + } // Check for @group tag (handle re-exports by looking at the original declaration) let targetDeclaration = declaration; @@ -714,7 +743,8 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) ? `${variableName}(\n${params.join(',\n')},\n): ${returnType};` : `${variableName}(): ${returnType};`; - let markdown = `${buildFrontmatter(description)}\n\n\n\n# ${variableName}\n\n\`\`\`ts\n${functionSig}\n\`\`\`${description ? `\n\n${description}` : ''}`; + const deprecationNotice = getDeprecationNotice(declaration, variableName); + let markdown = `${buildFrontmatter(description)}\n\n\n\n# ${variableName}\n\n${deprecationNotice}\`\`\`ts\n${functionSig}\n\`\`\`${description ? `\n\n${description}` : ''}`; // Find referenced types in all parameters and return type const allTypeStrings = params.map(p => p.replace(/\t.*?:\s*/, '')).concat([returnType]); @@ -728,7 +758,8 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) } } else { // Handle regular variables - let markdown = `${buildFrontmatter(description)}\n\n\n\n# ${variableName}\n\n${description ? `${description}\n\n` : ''}`; + const deprecationNotice = getDeprecationNotice(declaration, variableName); + let markdown = `${buildFrontmatter(description)}\n\n\n\n# ${variableName}\n\n${deprecationNotice}${description ? `${description}\n\n` : ''}`; const variableValue = declaration.initializer ? declaration.initializer.getText() : 'undefined'; const variableDefinition = `const ${variableName} = ${variableValue};`; markdown += `\`\`\`ts\n${variableDefinition}\n\`\`\``; @@ -971,33 +1002,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) const isDeprecatedMember = hasDeprecatedTag(member); - const getDeprecationNotice = () => { - const tag = ts.getJSDocTags(member).find(t => t.tagName.text === 'deprecated'); - let text = ''; - if (tag?.comment) { - if (typeof tag.comment === 'string') { - text = processLinkTags(tag.comment.trim(), className); - } else { - // comment is a NodeArray of JSDocComment elements (text + inline tags) - const raw = tag.comment.map((part) => { - if (ts.isJSDocLinkLike(part)) { - const linkName = part.name?.getText() ?? ''; - const linkText = part.text?.trim() ?? ''; - // Reconstruct as {@link Name text} - return `{@link ${linkName}${linkText ? ' ' + linkText : ''}}`; - } - return part.text ?? ''; - }).join(''); - text = processLinkTags(raw.trim(), className); - } - } - return text ? `> **Deprecated.** ${text}\n\n` : '> **Deprecated.**\n\n'; - }; - const addDeprecationNotice = (content: string) => { // Insert the deprecation notice right after the heading line const headingEnd = content.indexOf('\n'); - return content.slice(0, headingEnd) + '\n\n' + getDeprecationNotice() + content.slice(headingEnd + 1); + return content.slice(0, headingEnd) + '\n\n' + getDeprecationNotice(member, className) + content.slice(headingEnd + 1); }; const pushProperty = (content: string) => { @@ -1529,7 +1537,8 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) markdown += `\n\n`; } - markdown += `# ${className}\n\n${description ? `${description}\n` : ''}${extendsClause}${implementsClause}`; + const deprecationNotice = getDeprecationNotice(declaration, className); + markdown += `# ${className}\n\n${deprecationNotice}${description ? `${description}\n` : ''}${extendsClause}${implementsClause}`; // Add subclasses section for classes that have subclasses if (ts.isClassDeclaration(declaration) && classHierarchy.has(className)) { @@ -1900,7 +1909,25 @@ const extractJsDocDescription = ( descLines.push(line); } } else { - descLines = lines.filter(line => !line.trim().startsWith('@')); + // Skip @tag lines and their continuation lines (continuation ends at a blank line + // or the next @tag). Without this, multi-line tags like @deprecated bleed into the + // description. + descLines = []; + let inTagContinuation = false; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('@')) { + inTagContinuation = true; + continue; + } + if (inTagContinuation) { + if (trimmed === '') { + inTagContinuation = false; + } + continue; + } + descLines.push(line); + } } const rawDesc = descLines.join('\n').trim(); diff --git a/src/index.ts b/src/index.ts index 4e5c9f0..f87c1bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -173,10 +173,14 @@ export { type BlobSourceOptions, BufferSource, CustomPathedSource, + CustomSource, + type CustomSourceOptions, FilePathSource, type FilePathSourceOptions, PathedSource, + // eslint-disable-next-line @typescript-eslint/no-deprecated StreamSource, + // eslint-disable-next-line @typescript-eslint/no-deprecated type StreamSourceOptions, RangedSource, ReadableStreamSource, diff --git a/src/source.ts b/src/source.ts index 747bc9e..434a0b1 100644 --- a/src/source.ts +++ b/src/source.ts @@ -95,7 +95,7 @@ export abstract class Source extends EventEmitter { /** * FinalizationRegistry for rogue refs to this source that didn't get freed. It lives on the Source itself so that * in case the Source transitively points back to itself and forms a cycle (for example through a custom - * StreamSource callback) that we're not leaking memory. + * CustomSource callback) that we're not leaking memory. * @internal */ _refFinalizationRegistry: FinalizationRegistry | null = null; @@ -196,10 +196,12 @@ export abstract class Source extends EventEmitter { return new SourceRef(this); } + /** @internal */ _incrementRefCount() { this._refCount++; } + /** @internal */ _decrementRefCount() { this._refCount--; @@ -990,7 +992,7 @@ export type FilePathSourceOptions = { */ export class FilePathSource extends PathedSource { /** @internal */ - _streamSource: StreamSource; + _customSource: CustomSource; /** @internal */ _fileHandle: FileHandle | null = null; @@ -1017,8 +1019,8 @@ export class FilePathSource extends PathedSource { super(filePath, request => new FilePathSource(request.path, options)); - // Let's back this source with a StreamSource, makes the implementation very simple - this._streamSource = new StreamSource({ + // Let's back this source with a CustomSource, makes the implementation very simple + this._customSource = new CustomSource({ getSize: async () => { const fileHandle = await node.fs.open(filePath, 'r'); this._fileHandle = fileHandle; @@ -1051,17 +1053,17 @@ export class FilePathSource extends PathedSource { minReadPosition: number, maxReadPosition: number, ): MaybePromise { - return this._streamSource._read(start, end, minReadPosition, maxReadPosition); + return this._customSource._read(start, end, minReadPosition, maxReadPosition); } /** @internal */ _getFileSize(): number | null | undefined { - return this._streamSource._getFileSize(); + return this._customSource._getFileSize(); } /** @internal */ _dispose() { - this._streamSource._dispose(); + this._customSource._dispose(); if (this._fileHandle) { void this._fileHandle.close(); @@ -1072,11 +1074,11 @@ export class FilePathSource extends PathedSource { } /** - * Options for defining a {@link StreamSource}. + * Options for defining a {@link CustomSource}. * @group Input sources * @public */ -export type StreamSourceOptions = { +export type CustomSourceOptions = { /** * Called when the size of the entire file is requested. Must return or resolve to the size in bytes. This function * is guaranteed to be called before `read`. @@ -1086,6 +1088,8 @@ export type StreamSourceOptions = { /** * Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream * that yields these bytes. + * + * You are guaranteed that `0 <= start < end < fileSize`. */ read: (start: number, end: number) => MaybePromise>; @@ -1112,18 +1116,19 @@ export type StreamSourceOptions = { }; /** - * A general-purpose, callback-driven source that can get its data from anywhere. + * A general-purpose, callback-driven source that can get its data from anywhere. Use this source to implement your own + * custom source if the other sources don't cover your case. * @group Input sources * @public */ -export class StreamSource extends Source { +export class CustomSource extends Source { /** @internal */ - _options: StreamSourceOptions; + _options: CustomSourceOptions; /** @internal */ _orchestrator: ReadOrchestrator; - /** Creates a new {@link StreamSource} whose behavior is specified by `options`. */ - constructor(options: StreamSourceOptions) { + /** Creates a new {@link CustomSource} whose behavior is specified by `options`. */ + constructor(options: CustomSourceOptions) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); } @@ -1271,6 +1276,25 @@ export class StreamSource extends Source { } } +/** + * An alias for {@link CustomSource}. + * @deprecated This name is misleading and will be removed in a future release. Please use {@link CustomSource} instead. + * + * @group Input sources + * @public + */ +export const StreamSource = CustomSource; + +/** + * An alias for {@link CustomSourceOptions}. + * @deprecated This name is misleading and will be removed in a future release. Please use + * {@link CustomSourceOptions} instead. + * + * @group Input sources + * @public + */ +export type StreamSourceOptions = CustomSourceOptions; + type ReadableStreamSourcePendingSlice = { start: number; end: number; diff --git a/test/node/mpeg-ts-demuxing.test.ts b/test/node/mpeg-ts-demuxing.test.ts index e178b74..4936dc3 100644 --- a/test/node/mpeg-ts-demuxing.test.ts +++ b/test/node/mpeg-ts-demuxing.test.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest'; import { Input } from '../../src/input.js'; -import { FilePathSource, ReadableStreamSource, StreamSource } from '../../src/source.js'; +import { FilePathSource, ReadableStreamSource, CustomSource } from '../../src/source.js'; import path from 'node:path'; import fs from 'node:fs'; import { Readable } from 'node:stream'; @@ -759,7 +759,7 @@ test('MPEG-TS partial reading', async () => { let maxEnd = 0; using input = new Input({ - source: new StreamSource({ + source: new CustomSource({ getSize: () => { return buffer.byteLength; },