Add media conversion utility function, fix fragment/cluster reading issues, expand color space logic, add qualitative bitrates, & many other fixes and improvements

This commit is contained in:
Vanilagy
2025-01-19 18:55:12 +01:00
parent d323c2be8e
commit 70b88332b2
19 changed files with 1504 additions and 217 deletions
+1 -1
View File
@@ -450,7 +450,7 @@ export class EBMLReader {
}
if (width !== 4 && width !== 8) {
throw new Error('Bad FLOAT size ' + width);
throw new Error('Bad float size ' + width);
}
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + width);
+56 -14
View File
@@ -69,6 +69,7 @@ type Cluster = {
timestamp: number;
trackData: Map<number, ClusterTrackData>;
nextCluster: Cluster | null;
isKnownToBeFirstCluster: boolean;
};
type ClusterTrackData = {
@@ -415,6 +416,7 @@ export class MatroskaDemuxer extends Demuxer {
timestamp: -1,
trackData: new Map(),
nextCluster: null,
isKnownToBeFirstCluster: false,
};
this.currentCluster = cluster;
this.readContiguousElements(this.clusterReader, size);
@@ -772,7 +774,7 @@ export class MatroskaDemuxer extends Demuxer {
const matrixCoefficients = reader.readUnsignedInt(size);
const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients] ?? null;
this.currentTrack.info.colorSpace.matrix = mapped;
this.currentTrack.info.colorSpace.matrix = mapped as VideoColorSpaceInit['matrix'];
}; break;
case EBMLId.Range: {
@@ -786,7 +788,7 @@ export class MatroskaDemuxer extends Demuxer {
const transferCharacteristics = reader.readUnsignedInt(size);
const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics] ?? null;
this.currentTrack.info.colorSpace.transfer = mapped;
this.currentTrack.info.colorSpace.transfer = mapped as VideoColorSpaceInit['transfer'];
}; break;
case EBMLId.Primaries: {
@@ -794,7 +796,7 @@ export class MatroskaDemuxer extends Demuxer {
const primaries = reader.readUnsignedInt(size);
const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries] ?? null;
this.currentTrack.info.colorSpace.primaries = mapped;
this.currentTrack.info.colorSpace.primaries = mapped as VideoColorSpaceInit['primaries'];
}; break;
case EBMLId.Projection: {
@@ -967,6 +969,10 @@ abstract class MatroskaTrackBacking<
constructor(public internalTrack: InternalTrack) {}
getId() {
return this.internalTrack.id;
}
getCodec(): Promise<MediaCodec | null> {
throw new Error('Not implemented on base class.');
}
@@ -996,14 +1002,35 @@ abstract class MatroskaTrackBacking<
async getFirstSample(options: SampleRetrievalOptions) {
return this.performClusterLookup(
() => {
const cluster = this.internalTrack.clusters[0];
const startCluster = this.internalTrack.segment.clusters[0] ?? null;
if (startCluster?.isKnownToBeFirstCluster) {
// Walk from the very first cluster in the file until we find one with our track in it
let currentCluster: Cluster | null = startCluster;
while (currentCluster) {
const trackData = currentCluster.trackData.get(this.internalTrack.id);
if (trackData) {
return {
clusterIndex: binarySearchExact(
this.internalTrack.clusters,
currentCluster.elementStartPos,
x => x.elementStartPos,
),
blockIndex: 0,
correctBlockFound: true,
};
}
currentCluster = currentCluster.nextCluster;
}
}
return {
clusterIndex: cluster ? 0 : -1,
blockIndex: cluster ? 0 : -1,
correctBlockFound: !!cluster,
clusterIndex: -1,
blockIndex: -1,
correctBlockFound: false,
};
},
0,
-Infinity, // Use -Infinity as a search timestamp to avoid using the cues
Infinity,
options,
);
@@ -1300,8 +1327,11 @@ abstract class MatroskaTrackBacking<
);
const cuePoint = cuePointIndex !== -1 ? this.internalTrack.cuePoints[cuePointIndex]! : null;
let nextClusterIsFirstCluster = false;
if (clusterIndex === -1) {
metadataReader.pos = cuePoint?.clusterPosition ?? segment.clusterSeekStartPos;
nextClusterIsFirstCluster = metadataReader.pos === segment.clusterSeekStartPos;
} else {
const cluster = this.internalTrack.clusters[clusterIndex]!;
@@ -1344,15 +1374,18 @@ abstract class MatroskaTrackBacking<
// This is the first time we've seen this cluster
metadataReader.pos = elementStartPos;
cluster = await demuxer.readCluster(segment);
if (prevCluster) prevCluster.nextCluster = cluster;
prevCluster = cluster;
} else {
// We already know this cluster
cluster = segment.clusters[index]!;
// Even if we already know the cluster, we might not yet know its predecessor
if (prevCluster) prevCluster.nextCluster = cluster;
prevCluster = cluster;
}
// Even if we already know the cluster, we might not yet know its predecessor, so always do this
if (prevCluster) prevCluster.nextCluster = cluster;
prevCluster = cluster;
if (nextClusterIsFirstCluster) {
cluster.isKnownToBeFirstCluster = true;
nextClusterIsFirstCluster = false;
}
const { clusterIndex, blockIndex, correctBlockFound } = getBestMatch();
@@ -1417,6 +1450,15 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking<EncodedVideoSample>
return this.internalTrack.info.rotation;
}
async getColorSpace(): Promise<VideoColorSpaceInit> {
return {
primaries: this.internalTrack.info.colorSpace?.primaries,
transfer: this.internalTrack.info.colorSpace?.transfer,
matrix: this.internalTrack.info.colorSpace?.matrix,
fullRange: this.internalTrack.info.colorSpace?.fullRange,
};
}
async getDecoderConfig(): Promise<VideoDecoderConfig | null> {
if (!this.internalTrack.info.codec) {
return null;
+8 -8
View File
@@ -31,7 +31,7 @@ import {
parseSubtitleTimestamp,
} from '../subtitles';
import {
PCM_CODECS,
PCM_AUDIO_CODECS,
PcmAudioCodec,
generateAv1CodecConfigurationFromCodecString,
parsePcmCodec,
@@ -308,7 +308,7 @@ export class MatroskaMuxer extends Muxer {
},
{
id: EBMLId.ProjectionPoseRoll,
data: (rotation + 180) % 360 - 180, // [0, 270] -> [-180, 90]
data: new EBMLFloat32((rotation + 180) % 360 - 180), // [0, 270] -> [-180, 90]
},
],
}
@@ -321,7 +321,7 @@ export class MatroskaMuxer extends Muxer {
}
private audioSpecificTrackInfo(trackData: MatroskaAudioTrackData) {
const pcmInfo = (PCM_CODECS as readonly string[]).includes(trackData.track.source._codec)
const pcmInfo = (PCM_AUDIO_CODECS as readonly string[]).includes(trackData.track.source._codec)
? parsePcmCodec(trackData.track.source._codec as PcmAudioCodec)
: null;
@@ -659,16 +659,16 @@ export class MatroskaMuxer extends Muxer {
// We can only finalize this cluster (and begin a new one) if we know that each track will be able to
// start the new one with a key frame.
const keyFrameQueuedEverywhere = this.trackDatas.every((otherTrackData) => {
if (otherTrackData.track.source._closed) {
return true;
}
if (trackData === otherTrackData) {
return chunk.type === 'key';
}
const firstQueuedSample = otherTrackData.chunkQueue[0];
return firstQueuedSample && firstQueuedSample.type === 'key';
if (firstQueuedSample) {
return firstQueuedSample.type === 'key';
}
return otherTrackData.track.source._closed;
});
if (