mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 16:53:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6b849ff8 | ||
|
|
b60d4cafed | ||
|
|
95091cce9c | ||
|
|
72ed933bf4 | ||
|
|
ef3b53ff8a | ||
|
|
ac8baa4873 |
@@ -98,6 +98,7 @@ const sponsors = {
|
|||||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||||
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
|
{ 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' },
|
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
|
||||||
|
{ image: 'https://avatars.githubusercontent.com/u/61233224', name: 'Allwhy', url: 'https://github.com/Allwhy' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const compressFile = async (file: File) => {
|
|||||||
|
|
||||||
// Display the final media file
|
// Display the final media file
|
||||||
videoElement.style.display = '';
|
videoElement.style.display = '';
|
||||||
videoElement.src = URL.createObjectURL(new Blob([output.target.buffer!]));
|
videoElement.src = URL.createObjectURL(new Blob([output.target.buffer!], { type: output.format.mimeType }));
|
||||||
void videoElement.play();
|
void videoElement.play();
|
||||||
|
|
||||||
compressionFacts.style.display = '';
|
compressionFacts.style.display = '';
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ const renderObject = (object: Record<string, unknown>) => {
|
|||||||
listItem.removeChild(loadingSpan);
|
listItem.removeChild(loadingSpan);
|
||||||
listItem.appendChild(renderValue(resolvedValue));
|
listItem.appendChild(renderValue(resolvedValue));
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
// Show the promise error
|
// Show the promise error
|
||||||
listItem.removeChild(loadingSpan);
|
listItem.removeChild(loadingSpan);
|
||||||
const errorSpan = document.createElement('span');
|
const errorSpan = document.createElement('span');
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ const generateVideo = async () => {
|
|||||||
videoInfo.style.display = '';
|
videoInfo.style.display = '';
|
||||||
|
|
||||||
// Display and play the resulting media file
|
// Display and play the resulting media file
|
||||||
const videoBlob = new Blob([output.target.buffer!], { type: 'video/mp4' });
|
const videoBlob = new Blob([output.target.buffer!], { type: output.format.mimeType });
|
||||||
resultVideo.src = URL.createObjectURL(videoBlob);
|
resultVideo.src = URL.createObjectURL(videoBlob);
|
||||||
void resultVideo.play();
|
void resultVideo.play();
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.4.0",
|
"version": "1.4.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.4.0",
|
"version": "1.4.4",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dom-mediacapture-transform": "^0.1.11",
|
"@types/dom-mediacapture-transform": "^0.1.11",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.4.0",
|
"version": "1.4.4",
|
||||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/bundles/mediabunny.cjs",
|
"main": "./dist/bundles/mediabunny.cjs",
|
||||||
|
|||||||
@@ -1941,7 +1941,12 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFirstPacket(options: PacketRetrievalOptions) {
|
async getFirstPacket(options: PacketRetrievalOptions) {
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
const regularPacket = await this.fetchPacketForSampleIndex(0, options);
|
||||||
|
if (regularPacket || !this.internalTrack.demuxer.isFragmented) {
|
||||||
|
// If there's a non-fragmented packet, always prefer that
|
||||||
|
return regularPacket;
|
||||||
|
}
|
||||||
|
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
() => {
|
() => {
|
||||||
const startFragment = this.internalTrack.demuxer.fragments[0] ?? null;
|
const startFragment = this.internalTrack.demuxer.fragments[0] ?? null;
|
||||||
@@ -1978,9 +1983,6 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.fetchPacketForSampleIndex(0, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapTimestampIntoTimescale(timestamp: number) {
|
private mapTimestampIntoTimescale(timestamp: number) {
|
||||||
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
|
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
|
||||||
// that the number was originally an integer divided by the timescale. For stability, it's best
|
// that the number was originally an integer divided by the timescale. For stability, it's best
|
||||||
@@ -1991,22 +1993,31 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||||
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
||||||
|
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
|
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
||||||
|
const regularPacket = await this.fetchPacketForSampleIndex(sampleIndex, options);
|
||||||
|
|
||||||
|
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
|
||||||
|
// Prefer the non-fragmented packet
|
||||||
|
return regularPacket;
|
||||||
|
}
|
||||||
|
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
() => this.findSampleInFragmentsForTimestamp(timestampInTimescale),
|
() => this.findSampleInFragmentsForTimestamp(timestampInTimescale),
|
||||||
timestampInTimescale,
|
timestampInTimescale,
|
||||||
timestampInTimescale,
|
timestampInTimescale,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
|
||||||
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
|
||||||
return this.fetchPacketForSampleIndex(sampleIndex, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
const regularSampleIndex = this.packetToSampleIndex.get(packet);
|
||||||
|
|
||||||
|
if (regularSampleIndex !== undefined) {
|
||||||
|
// Prefer the non-fragmented packet
|
||||||
|
return this.fetchPacketForSampleIndex(regularSampleIndex + 1, options);
|
||||||
|
}
|
||||||
|
|
||||||
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
||||||
if (locationInFragment === undefined) {
|
if (locationInFragment === undefined) {
|
||||||
throw new Error('Packet was not created from this track.');
|
throw new Error('Packet was not created from this track.');
|
||||||
@@ -2067,17 +2078,21 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleIndex = this.packetToSampleIndex.get(packet);
|
|
||||||
if (sampleIndex === undefined) {
|
|
||||||
throw new Error('Packet was not created from this track.');
|
|
||||||
}
|
|
||||||
return this.fetchPacketForSampleIndex(sampleIndex + 1, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||||
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
||||||
|
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
|
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
||||||
|
const keyFrameSampleIndex = sampleIndex === -1
|
||||||
|
? -1
|
||||||
|
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
|
||||||
|
const regularPacket = await this.fetchPacketForSampleIndex(keyFrameSampleIndex, options);
|
||||||
|
|
||||||
|
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
|
||||||
|
// Prefer the non-fragmented packet
|
||||||
|
return regularPacket;
|
||||||
|
}
|
||||||
|
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
() => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale),
|
() => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale),
|
||||||
timestampInTimescale,
|
timestampInTimescale,
|
||||||
@@ -2086,16 +2101,15 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
||||||
|
const regularSampleIndex = this.packetToSampleIndex.get(packet);
|
||||||
|
if (regularSampleIndex !== undefined) {
|
||||||
|
// Prefer the non-fragmented packet
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex);
|
||||||
const keyFrameSampleIndex = sampleIndex === -1
|
return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
|
||||||
? -1
|
|
||||||
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
|
|
||||||
return this.fetchPacketForSampleIndex(keyFrameSampleIndex, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
|
||||||
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
||||||
if (locationInFragment === undefined) {
|
if (locationInFragment === undefined) {
|
||||||
throw new Error('Packet was not created from this track.');
|
throw new Error('Packet was not created from this track.');
|
||||||
@@ -2163,15 +2177,6 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleIndex = this.packetToSampleIndex.get(packet);
|
|
||||||
if (sampleIndex === undefined) {
|
|
||||||
throw new Error('Packet was not created from this track.');
|
|
||||||
}
|
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
|
||||||
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex);
|
|
||||||
return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchPacketForSampleIndex(sampleIndex: number, options: PacketRetrievalOptions) {
|
private async fetchPacketForSampleIndex(sampleIndex: number, options: PacketRetrievalOptions) {
|
||||||
if (sampleIndex === -1) {
|
if (sampleIndex === -1) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2701,3 +2706,7 @@ const extractRotationFromMatrix = (matrix: TransformationMatrix) => {
|
|||||||
// Invert the rotation because matrices are post-multiplied in ISOBMFF
|
// Invert the rotation because matrices are post-multiplied in ISOBMFF
|
||||||
return -Math.atan2(sinTheta, cosTheta) * (180 / Math.PI);
|
return -Math.atan2(sinTheta, cosTheta) * (180 / Math.PI);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sampleTableIsEmpty = (sampleTable: SampleTable) => {
|
||||||
|
return sampleTable.sampleSizes.length === 0;
|
||||||
|
};
|
||||||
|
|||||||
@@ -220,13 +220,16 @@ export class MatroskaDemuxer extends Demuxer {
|
|||||||
|
|
||||||
const fileSize = await this.input.source.getSize();
|
const fileSize = await this.input.source.getSize();
|
||||||
|
|
||||||
|
// Loop over all top-level elements in the file
|
||||||
while (this.metadataReader.pos <= fileSize - MIN_HEADER_SIZE) {
|
while (this.metadataReader.pos <= fileSize - MIN_HEADER_SIZE) {
|
||||||
await this.metadataReader.reader.loadRange(
|
await this.metadataReader.reader.loadRange(
|
||||||
this.metadataReader.pos,
|
this.metadataReader.pos,
|
||||||
this.metadataReader.pos + MAX_HEADER_SIZE,
|
this.metadataReader.pos + MAX_HEADER_SIZE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { id, size } = this.metadataReader.readElementHeader();
|
const header = this.metadataReader.readElementHeader();
|
||||||
|
const id = header.id;
|
||||||
|
let size = header.size;
|
||||||
const startPos = this.metadataReader.pos;
|
const startPos = this.metadataReader.pos;
|
||||||
|
|
||||||
if (id === EBMLId.EBML) {
|
if (id === EBMLId.EBML) {
|
||||||
@@ -242,6 +245,26 @@ export class MatroskaDemuxer extends Demuxer {
|
|||||||
// and only segment
|
// and only segment
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else if (id === EBMLId.Cluster) {
|
||||||
|
// Clusters are not a top-level element in Matroska, but some files contain a Segment whose size
|
||||||
|
// doesn't contain any of the clusters that follow it. In the case, we apply the following logic: if
|
||||||
|
// we find a top-level cluster, attribute it to the previous segment.
|
||||||
|
|
||||||
|
if (size === null) {
|
||||||
|
// Just in case this is one of those weird sizeless clusters, let's do our best and still try to
|
||||||
|
// determine its size.
|
||||||
|
const nextElementPos = await this.clusterReader.searchForNextElementId(
|
||||||
|
LEVEL_0_AND_1_EBML_IDS,
|
||||||
|
fileSize,
|
||||||
|
);
|
||||||
|
size = (nextElementPos ?? fileSize) - startPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSegment = last(this.segments);
|
||||||
|
if (lastSegment) {
|
||||||
|
// Extend the previous segment's size
|
||||||
|
lastSegment.elementEndPos = startPos + size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertDefinedSize(size);
|
assertDefinedSize(size);
|
||||||
@@ -368,6 +391,13 @@ export class MatroskaDemuxer extends Demuxer {
|
|||||||
this.readContiguousElements(this.metadataReader, size);
|
this.readContiguousElements(this.metadataReader, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.currentSegment.timestampScale === -1) {
|
||||||
|
// TimestampScale element is missing. Technically an invalid file, but let's default to the typical value,
|
||||||
|
// which is 1e6.
|
||||||
|
this.currentSegment.timestampScale = 1e6;
|
||||||
|
this.currentSegment.timestampFactor = 1e9 / 1e6;
|
||||||
|
}
|
||||||
|
|
||||||
// Put default tracks first
|
// Put default tracks first
|
||||||
this.currentSegment.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
|
this.currentSegment.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
|
||||||
|
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ export class VideoSample {
|
|||||||
return new VideoSample(this._data.clone(), {
|
return new VideoSample(this._data.clone(), {
|
||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
|
rotation: this.rotation,
|
||||||
});
|
});
|
||||||
} else if (this._data instanceof Uint8Array) {
|
} else if (this._data instanceof Uint8Array) {
|
||||||
return new VideoSample(this._data.slice(), {
|
return new VideoSample(this._data.slice(), {
|
||||||
@@ -245,6 +246,7 @@ export class VideoSample {
|
|||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
colorSpace: this.colorSpace,
|
colorSpace: this.colorSpace,
|
||||||
|
rotation: this.rotation,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new VideoSample(this._data, {
|
return new VideoSample(this._data, {
|
||||||
@@ -254,6 +256,7 @@ export class VideoSample {
|
|||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
colorSpace: this.colorSpace,
|
colorSpace: this.colorSpace,
|
||||||
|
rotation: this.rotation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user