Add limited edit list support

This commit is contained in:
Vanilagy
2025-02-09 17:21:41 +01:00
parent f750bcfa21
commit 6cdaa06c07
4 changed files with 69 additions and 16 deletions
+6 -1
View File
@@ -28,6 +28,9 @@
format: outputFormat,
target
}),
audio: {
//discard: true
},
/*
video: {
discard: true,
@@ -43,6 +46,7 @@
codec: 'aac',
},
*/
/*
video: {
codec: 'avc',
},
@@ -50,9 +54,10 @@
codec: 'mp3',
bitrate: 320000
},
*/
trim: {
start: 0,
end: 60
end: 20
},
onProgress: (event) => {
progress.value = event.completion;
+10 -7
View File
@@ -341,7 +341,8 @@ class Conversion {
height = ceilToMultipleOfTwo(this.options.video.height);
}
const needsReencode = !!this.options.video?.forceReencode || this.startTimestamp > 0;
const firstTimestamp = await track.getFirstTimestamp();
const needsReencode = !!this.options.video?.forceReencode || this.startTimestamp > 0 || firstTimestamp < 0;
const needsResize = width !== originalWidth || height !== originalHeight;
let videoCodecs = this.output.format.getSupportedVideoCodecs();
@@ -513,11 +514,14 @@ class Conversion {
const originalNumberOfChannels = track.numberOfChannels;
const originalSampleRate = track.sampleRate;
const firstTimestamp = await track.getFirstTimestamp();
let numberOfChannels = this.options.audio?.numberOfChannels ?? originalNumberOfChannels;
let sampleRate = this.options.audio?.sampleRate ?? originalSampleRate;
let needsResample = numberOfChannels !== originalNumberOfChannels
|| sampleRate !== originalSampleRate
|| this.startTimestamp > 0;
|| this.startTimestamp > 0
|| firstTimestamp < 0;
let audioCodecs = this.output.format.getSupportedAudioCodecs();
if (
@@ -671,13 +675,12 @@ class Conversion {
await track.computeDuration() - this.startTimestamp,
this.endTimestamp - this.startTimestamp,
);
const totalFrameCount = Math.round(trackDuration * FALLBACK_SAMPLE_RATE);
const MAX_CHUNK_LENGTH = 5 * FALLBACK_SAMPLE_RATE;
const totalFrameCount = Math.round(trackDuration * targetSampleRate);
const maxChunkLength = 5 * targetSampleRate;
let currentContextStartFrame = 0;
let currentContext: OfflineAudioContext | null = new OfflineAudioContext({
length: Math.min(totalFrameCount - currentContextStartFrame, MAX_CHUNK_LENGTH),
length: Math.min(totalFrameCount - currentContextStartFrame, maxChunkLength),
numberOfChannels: targetNumberOfChannels,
sampleRate: targetSampleRate,
});
@@ -719,7 +722,7 @@ class Conversion {
const newLength = Math.min(
totalFrameCount - currentContextStartFrame,
MAX_CHUNK_LENGTH,
maxChunkLength,
);
currentContext = newLength > 0
? new OfflineAudioContext({
+53 -7
View File
@@ -57,6 +57,7 @@ type InternalTrack = {
fragmentLookupTable: FragmentLookupTableEntry[] | null;
currentFragmentState: FragmentTrackState | null;
fragments: Fragment[];
editListOffset: number;
} & ({
info: null;
} | {
@@ -547,7 +548,8 @@ export class IsobmffDemuxer extends Demuxer {
case 'mdia':
case 'minf':
case 'dinf':
case 'mfra': {
case 'mfra':
case 'edts': {
this.readContiguousBoxes(boxInfo.contentSize);
}; break;
@@ -582,6 +584,7 @@ export class IsobmffDemuxer extends Demuxer {
fragmentLookupTable: null,
currentFragmentState: null,
fragments: [],
editListOffset: 0,
} satisfies InternalTrack as InternalTrack;
this.currentTrack = track;
@@ -654,6 +657,47 @@ export class IsobmffDemuxer extends Demuxer {
}
}; break;
case 'elst': {
const track = this.currentTrack;
assert(track);
const version = this.metadataReader.readU8();
this.metadataReader.pos += 3; // Flags
let relevantEntryFound = false;
const entryCount = this.metadataReader.readU32();
for (let i = 0; i < entryCount; i++) {
const segmentDuration = version === 1
? this.metadataReader.readU64()
: this.metadataReader.readU32();
const mediaTime = version === 1
? this.metadataReader.readI64()
: this.metadataReader.readI32();
const mediaRate = this.metadataReader.readFixed_16_16();
if (segmentDuration === 0) {
// Don't care
continue;
}
if (relevantEntryFound) {
throw new Error('Unsupported edit list: multiple edits are not supported.');
}
if (mediaTime === -1) {
throw new Error('Unsupported edit list: no empty edits allowed.');
}
if (mediaRate !== 1) {
throw new Error('Unsupported edit list: media rate must be 1.');
}
track.editListOffset = mediaTime;
relevantEntryFound = true;
}
}; break;
case 'mdhd': {
const track = this.currentTrack;
assert(track);
@@ -1796,15 +1840,15 @@ abstract class IsobmffTrackBacking<
return this.fetchSampleForSampleIndex(0, options);
}
private intoTimescale(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
// that the number was originally an integer divided by the timescale. For stability, it's best
// to return the integer in this case.
return roundToPrecision(timestamp * this.internalTrack.timescale, 14);
return roundToPrecision(timestamp * this.internalTrack.timescale, 14) + this.internalTrack.editListOffset;
}
async getSample(timestamp: number, options: SampleRetrievalOptions) {
const timestampInTimescale = this.intoTimescale(timestamp);
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
@@ -1890,7 +1934,7 @@ abstract class IsobmffTrackBacking<
}
async getKeySample(timestamp: number, options: SampleRetrievalOptions) {
const timestampInTimescale = this.intoTimescale(timestamp);
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
@@ -2014,7 +2058,8 @@ abstract class IsobmffTrackBacking<
data = this.internalTrack.demuxer.chunkReader.readBytes(sampleInfo.sampleSize);
}
const timestamp = sampleInfo.presentationTimestamp / this.internalTrack.timescale;
const timestamp = (sampleInfo.presentationTimestamp - this.internalTrack.editListOffset)
/ this.internalTrack.timescale;
const duration = sampleInfo.duration / this.internalTrack.timescale;
const sample = this.createSample(
data,
@@ -2049,7 +2094,8 @@ abstract class IsobmffTrackBacking<
data = this.internalTrack.demuxer.chunkReader.readBytes(fragmentSample.byteSize);
}
const timestamp = fragmentSample.presentationTimestamp / this.internalTrack.timescale;
const timestamp = (fragmentSample.presentationTimestamp - this.internalTrack.editListOffset)
/ this.internalTrack.timescale;
const duration = fragmentSample.duration / this.internalTrack.timescale;
const sample = this.createSample(
data,
-1
View File
@@ -1,4 +1,3 @@
- Throw if edit list detected
- A stream source?? Or like a callback-driven source
- onHeader, etc callbacks for Matroska
- https://github.com/Vanilagy/mp4-muxer/issues/83 tell him it's possible now