Code review fixes

This commit is contained in:
Vanilagy
2026-04-16 17:43:12 +02:00
parent aadfa77489
commit b85ecd3109
13 changed files with 253 additions and 44 deletions
+7 -3
View File
@@ -402,7 +402,9 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions) => {
|| (Array.isArray(videoOptions.group) && videoOptions.group.every(x => x instanceof OutputTrackGroup))
)
) {
throw new TypeError('options.video.group, when provided, must be a string or an array of strings.');
throw new TypeError(
'options.video.group, when provided, must be an OutputTrackGroup or an array of OutputTrackGroups.',
);
}
};
@@ -462,7 +464,9 @@ const validateAudioOptions = (audioOptions: ConversionAudioOptions) => {
|| (Array.isArray(audioOptions.group) && audioOptions.group.every(x => x instanceof OutputTrackGroup))
)
) {
throw new TypeError('options.audio.group, when provided, must be a string or an array of strings.');
throw new TypeError(
'options.audio.group, when provided, must be an OutputTrackGroup or an array of OutputTrackGroups.',
);
}
};
@@ -612,7 +616,7 @@ export class Conversion {
&& options.tracks !== 'primary'
) {
throw new TypeError(
'options.tracks, when provded, must be either \'all\' or \'primary\'.',
'options.tracks, when provided, must be either \'all\' or \'primary\'.',
);
}
if (
+3 -3
View File
@@ -203,7 +203,7 @@ export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
throw new TypeError('config.transform.rotate, when provided, must be 0, 90, 180 or 270.');
}
if (config.transform.crop !== undefined) {
validateCropRectangle(config.transform.crop, 'config.');
validateCropRectangle(config.transform.crop, 'config.transform.');
}
if (config.transform.process !== undefined && typeof config.transform.process !== 'function') {
throw new TypeError('config.transform.process, when provided, must be a function.');
@@ -219,7 +219,7 @@ export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
}
}
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
throw new TypeError('config.onEncodedPacket, when provided, must be a function.');
}
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
@@ -438,7 +438,7 @@ export const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
}
}
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
throw new TypeError('config.onEncodedPacket, when provided, must be a function.');
}
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
+1 -1
View File
@@ -496,7 +496,7 @@ export class HlsDemuxer extends Demuxer {
const channels = mediaTag.attributes.get('channels')
?? variantStream.attributes.get('channels');
const parsedChannels = channels !== null
? Number(channels)
? Number(channels.split('/')[0]!)
: null;
result.push({
+31 -22
View File
@@ -294,19 +294,21 @@ export class HlsMuxer extends Muxer {
}
}
const getMetadataKeyForTrack = ({ metadata }: OutputTrack) => {
let key = '';
key += `${metadata.languageCode ?? UNDETERMINED_LANGUAGE}-`;
key += `${metadata.name ?? ''}-`;
key += `${metadata.disposition?.default ?? true}-`;
key += `${metadata.disposition?.primary ?? false}-`;
key += `${metadata.disposition?.forced ?? false}-`;
return key;
};
// Video tracks that can't be paired with any other track always live on the top-level, the question is just if
// they need to be separated into #EXT-X-MEDIA tags or not
if (unpairedVideoTracks.length > 0) {
const uniqueMetadata = new Set(unpairedVideoTracks.map(({ metadata }) => {
let key = '';
key += `${metadata.languageCode ?? UNDETERMINED_LANGUAGE}-`;
key += `${metadata.name ?? ''}-`;
key += `${metadata.disposition?.default ?? true}-`;
key += `${metadata.disposition?.primary ?? false}-`;
key += `${metadata.disposition?.forced ?? false}-`;
return key;
}));
const uniqueMetadata = new Set(unpairedVideoTracks.map(getMetadataKeyForTrack));
if (uniqueMetadata.size > 1) {
// They differ in metadata, emit as group
@@ -336,16 +338,7 @@ export class HlsMuxer extends Muxer {
// Audio tracks that can't be paired with any other track always live on the top-level, the question is just if
// they need to be separated into #EXT-X-MEDIA tags or not
if (unpairedAudioTracks.length > 0) {
const uniqueMetadata = new Set(unpairedAudioTracks.map(({ metadata }) => {
let key = '';
key += `${metadata.languageCode ?? UNDETERMINED_LANGUAGE}-`;
key += `${metadata.name ?? ''}-`;
key += `${metadata.disposition?.default ?? true}-`;
key += `${metadata.disposition?.primary ?? false}-`;
key += `${metadata.disposition?.forced ?? false}-`;
return key;
}));
const uniqueMetadata = new Set(unpairedAudioTracks.map(getMetadataKeyForTrack));
if (uniqueMetadata.size > 1) {
// They differ in metadata, emit as group
@@ -686,6 +679,17 @@ export class HlsMuxer extends Muxer {
// Loop in case we can finalize multiple segments
while (true) {
// This here is the core segmentation logic. The segmentation logic figures out which packets are to be
// written into the next segment, and if we can write a segment at all. If tracks are still open and have
// not provided sufficient media data, no segment will be written. The packets will be added to the segment
// to maximize its duration AND keep it from exceeding the target duration. This condition is extended with
// a key frame rule for video, meaning the algorithm must guarantee that every segment with video data
// begins with a video key frame.
//
// The logic is quite complex but is solved in a straight-forward way: all possible permutations of the
// problem are checked in a nested if-else structure, making sure all cases behave correctly. This was the
// easiest, least error-prone way I found to express this behavior.
const currentSegmentEndTimestamp = playlist.currentSegmentStartTimestamp + this.targetSegmentDuration;
// These store the index (exclusive) until when packets can be added to the next segment
@@ -816,6 +820,9 @@ export class HlsMuxer extends Muxer {
if (this.singleFilePerPlaylist) {
if (playlist.singleFile === null) {
// INTENTIONALLY shadow the outside `segmentInfo` because we don't want to set it.
// In single-file mode, onSegment is called once in onPlaylistDone instead of per-segment,
// so the outer `segmentInfo` intentionally stays null in this case.
const segmentInfo: HlsOutputSegmentInfo = {
n: playlist.nextSegmentId,
format: playlist.segmentFormat,
@@ -1028,6 +1035,7 @@ export class HlsMuxer extends Muxer {
assert(Number.isFinite(nextSegmentStartTimestamp));
const segmentDuration = nextSegmentStartTimestamp - playlist.currentSegmentStartTimestamp;
assert(segmentDuration >= 0);
playlist.writtenSegments.push({
path: relativeSegmentPath,
@@ -1119,7 +1127,8 @@ export class HlsMuxer extends Muxer {
// Fallback: if no contiguous set falls within the range, use per-segment max
if (peakBitrate === 0) {
for (const segment of segments) {
peakBitrate = Math.max(peakBitrate, 8 * segment.byteSize / segment.duration);
const segmentDuration = segment.duration || 1; // To catch 0-duration segments which can happen
peakBitrate = Math.max(peakBitrate, 8 * segment.byteSize / segmentDuration);
}
}
@@ -1166,7 +1175,7 @@ export class HlsMuxer extends Muxer {
+ (!this.isLive ? '#EXT-X-PLAYLIST-TYPE:VOD\n' : '')
+ `#EXT-X-TARGETDURATION:${Math.ceil(targetDuration)}\n` // Must be a "decimal-integer"
+ (Number.isFinite(this.maxLiveSegmentCount) ? `#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequence}\n` : '')
+ '#EXT-X-INDEPENDENT-SEGMENTS\n' // Todo not for live?
+ '#EXT-X-INDEPENDENT-SEGMENTS\n'
+ (isKeyPacketsOnly ? '#EXT-X-I-FRAMES-ONLY\n' : '')
+ (playlist.initSegment
? (`#EXT-X-MAP:URI="${playlist.initSegment.path}"`
+1 -1
View File
@@ -187,7 +187,7 @@ export class HlsSegmentedInput extends SegmentedInput {
if (!line.startsWith('#')) {
if (!prevLastSegment) {
if (nextSegmentDuration === null) {
throw new Error('Invalid M3U8 file; a segment must be preceeded by a #EXTINF tag.');
throw new Error('Invalid M3U8 file; a segment must be preceded by an #EXTINF tag.');
}
let key = currentKey;
+20 -10
View File
@@ -13,23 +13,33 @@ export const readNextMp3FrameHeader = async (reader: Reader, startPos: number, u
header: Mp3FrameHeader;
startPos: number;
} | null> => {
const CHUNK_SIZE = 2 ** 16; // So we don't need to grab thousands of slices
let currentPos = startPos;
// todo optimize this shit wtf is this
while (until === null || currentPos < until) {
let slice = reader.requestSlice(currentPos, FRAME_HEADER_SIZE);
const maxLength = until !== null
? Math.min(CHUNK_SIZE, until - currentPos)
: CHUNK_SIZE;
let slice = reader.requestSliceRange(currentPos, FRAME_HEADER_SIZE, maxLength);
if (slice instanceof Promise) slice = await slice;
if (!slice) break;
if (!slice || slice.length < FRAME_HEADER_SIZE) break;
const word = readU32Be(slice);
while (slice.remainingLength >= FRAME_HEADER_SIZE) {
const posBeforeRead = slice.filePos;
const word = readU32Be(slice);
const remainingBytes = reader.fileSize !== null
? reader.fileSize - currentPos
: null;
const result = readMp3FrameHeader(word, reader.fileSize !== null ? reader.fileSize - currentPos : null);
if (result.header) {
return { header: result.header, startPos: currentPos };
const result = readMp3FrameHeader(word, remainingBytes);
if (result.header) {
return { header: result.header, startPos: currentPos };
}
slice.filePos = posBeforeRead + result.bytesAdvanced;
currentPos = slice.filePos;
}
currentPos += result.bytesAdvanced;
}
return null;
+3
View File
@@ -1342,6 +1342,9 @@ export class HlsOutputFormat extends OutputFormat {
if (options.getSegmentPath !== undefined && typeof options.getSegmentPath !== 'function') {
throw new TypeError('options.getSegmentPath, when provided, must be a function.');
}
if (options.getInitPath !== undefined && typeof options.getInitPath !== 'function') {
throw new TypeError('options.getInitPath, when provided, must be a function.');
}
if (options.onMaster !== undefined && typeof options.onMaster !== 'function') {
throw new TypeError('options.onMaster, when provided, must be a function.');
}
+2 -1
View File
@@ -445,7 +445,7 @@ export class Output<
&& typeof options.initTarget !== 'function'
) {
throw new Error(
'options.getInitTarget, when provided, must be a Target or a function that returns or resolves to'
'options.initTarget, when provided, must be a Target or a function that returns or resolves to'
+ ' a Target.',
);
}
@@ -550,6 +550,7 @@ export class Output<
target._output = this;
if (this.state === 'canceled') {
// Promise thrown away here, but no way to surface it to the user really
void target._close();
} else {
this._targets.add(target);
-1
View File
@@ -594,7 +594,6 @@ export class FilePathTarget extends Target {
chunked: true,
...options,
});
this._streamTarget._output = this._output;
}
/** @internal */