diff --git a/dev/convert.html b/dev/convert.html
index 4af9517..697c30c 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -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;
diff --git a/src/conversion.ts b/src/conversion.ts
index 51eb3a5..c7c7057 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -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({
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index 8362169..8985e8a 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -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,
diff --git a/todo.txt b/todo.txt
index fcb946a..c37bc4b 100644
--- a/todo.txt
+++ b/todo.txt
@@ -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
\ No newline at end of file