mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Fixed negative timestamps occurring with live media (fixes #364)
This commit is contained in:
@@ -60,7 +60,7 @@ export class AdtsMuxer extends Muxer {
|
|||||||
const release = await this.mutex.acquire();
|
const release = await this.mutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
this.validateTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||||
|
|
||||||
// First packet - determine input format from metadata
|
// First packet - determine input format from metadata
|
||||||
if (this.inputIsAdts === null) {
|
if (this.inputIsAdts === null) {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export class FlacMuxer extends Muxer {
|
|||||||
const release = await this.mutex.acquire();
|
const release = await this.mutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.validateAndNormalizeTimestamp(
|
this.validateTimestamp(
|
||||||
track,
|
track,
|
||||||
packet.timestamp,
|
packet.timestamp,
|
||||||
packet.type === 'key',
|
packet.type === 'key',
|
||||||
|
|||||||
+8
-12
@@ -614,17 +614,15 @@ export class HlsMuxer extends Muxer {
|
|||||||
const release = await playlist.mutex.acquire();
|
const release = await playlist.mutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
this.validateTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||||
const adjustedPacket = packet.clone({ timestamp });
|
trackData.packets.push(packet);
|
||||||
|
|
||||||
trackData.packets.push(adjustedPacket);
|
|
||||||
|
|
||||||
if (playlist.currentSegmentStartTimestamp === null) {
|
if (playlist.currentSegmentStartTimestamp === null) {
|
||||||
playlist.currentSegmentStartTimestamp = adjustedPacket.timestamp;
|
playlist.currentSegmentStartTimestamp = packet.timestamp;
|
||||||
} else if (!playlist.currentSegmentStartTimestampIsFixed) {
|
} else if (!playlist.currentSegmentStartTimestampIsFixed) {
|
||||||
playlist.currentSegmentStartTimestamp = Math.min(
|
playlist.currentSegmentStartTimestamp = Math.min(
|
||||||
playlist.currentSegmentStartTimestamp,
|
playlist.currentSegmentStartTimestamp,
|
||||||
adjustedPacket.timestamp,
|
packet.timestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,17 +643,15 @@ export class HlsMuxer extends Muxer {
|
|||||||
const release = await playlist.mutex.acquire();
|
const release = await playlist.mutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
this.validateTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||||
const adjustedPacket = packet.clone({ timestamp });
|
trackData.packets.push(packet);
|
||||||
|
|
||||||
trackData.packets.push(adjustedPacket);
|
|
||||||
|
|
||||||
if (playlist.currentSegmentStartTimestamp === null) {
|
if (playlist.currentSegmentStartTimestamp === null) {
|
||||||
playlist.currentSegmentStartTimestamp = adjustedPacket.timestamp;
|
playlist.currentSegmentStartTimestamp = packet.timestamp;
|
||||||
} else if (!playlist.currentSegmentStartTimestampIsFixed) {
|
} else if (!playlist.currentSegmentStartTimestampIsFixed) {
|
||||||
playlist.currentSegmentStartTimestamp = Math.min(
|
playlist.currentSegmentStartTimestamp = Math.min(
|
||||||
playlist.currentSegmentStartTimestamp,
|
playlist.currentSegmentStartTimestamp,
|
||||||
adjustedPacket.timestamp,
|
packet.timestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ export class IsobmffMuxer extends Muxer {
|
|||||||
packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4);
|
packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(
|
this.validateTimestamp(
|
||||||
trackData.track,
|
trackData.track,
|
||||||
packet.timestamp,
|
packet.timestamp,
|
||||||
packet.type === 'key',
|
packet.type === 'key',
|
||||||
@@ -608,7 +608,7 @@ export class IsobmffMuxer extends Muxer {
|
|||||||
const internalSample = this.createSampleForTrack(
|
const internalSample = this.createSampleForTrack(
|
||||||
trackData,
|
trackData,
|
||||||
packetData,
|
packetData,
|
||||||
timestamp,
|
packet.timestamp,
|
||||||
packet.duration,
|
packet.duration,
|
||||||
packet.type,
|
packet.type,
|
||||||
);
|
);
|
||||||
@@ -638,11 +638,13 @@ export class IsobmffMuxer extends Muxer {
|
|||||||
packetData = packetData.subarray(headerLength);
|
packetData = packetData.subarray(headerLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = this.validateAndNormalizeTimestamp(
|
this.validateTimestamp(
|
||||||
trackData.track,
|
trackData.track,
|
||||||
packet.timestamp,
|
packet.timestamp,
|
||||||
packet.type === 'key',
|
packet.type === 'key',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let timestamp = packet.timestamp;
|
||||||
let duration = packet.duration;
|
let duration = packet.duration;
|
||||||
|
|
||||||
if (trackData.info.requiresPcmTransformation) {
|
if (trackData.info.requiresPcmTransformation) {
|
||||||
@@ -718,7 +720,7 @@ export class IsobmffMuxer extends Muxer {
|
|||||||
try {
|
try {
|
||||||
const trackData = this.getSubtitleTrackData(track, meta);
|
const trackData = this.getSubtitleTrackData(track, meta);
|
||||||
|
|
||||||
this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true);
|
this.validateTimestamp(trackData.track, cue.timestamp, true);
|
||||||
|
|
||||||
if (track.source._codec === 'webvtt') {
|
if (track.source._codec === 'webvtt') {
|
||||||
trackData.cueQueue.push(cue);
|
trackData.cueQueue.push(cue);
|
||||||
|
|||||||
@@ -907,7 +907,9 @@ export class MatroskaMuxer extends Muxer {
|
|||||||
const trackData = this.getVideoTrackData(track, packet, meta);
|
const trackData = this.getVideoTrackData(track, packet, meta);
|
||||||
|
|
||||||
const isKeyFrame = packet.type === 'key';
|
const isKeyFrame = packet.type === 'key';
|
||||||
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
|
this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
|
||||||
|
|
||||||
|
let timestamp = packet.timestamp;
|
||||||
let duration = packet.duration;
|
let duration = packet.duration;
|
||||||
|
|
||||||
if (track.metadata.frameRate !== undefined) {
|
if (track.metadata.frameRate !== undefined) {
|
||||||
@@ -950,9 +952,9 @@ export class MatroskaMuxer extends Muxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isKeyFrame = packet.type === 'key';
|
const isKeyFrame = packet.type === 'key';
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
|
this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
|
||||||
const audioChunk = this.createInternalChunk(packetData, timestamp, packet.duration, packet.type);
|
|
||||||
|
|
||||||
|
const audioChunk = this.createInternalChunk(packetData, packet.timestamp, packet.duration, packet.type);
|
||||||
trackData.chunkQueue.push(audioChunk);
|
trackData.chunkQueue.push(audioChunk);
|
||||||
await this.interleaveChunks();
|
await this.interleaveChunks();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -966,10 +968,10 @@ export class MatroskaMuxer extends Muxer {
|
|||||||
try {
|
try {
|
||||||
const trackData = this.getSubtitleTrackData(track, meta);
|
const trackData = this.getSubtitleTrackData(track, meta);
|
||||||
|
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true);
|
this.validateTimestamp(trackData.track, cue.timestamp, true);
|
||||||
|
|
||||||
let bodyText = cue.text;
|
let bodyText = cue.text;
|
||||||
const timestampMs = Math.round(timestamp * 1000);
|
const timestampMs = Math.round(cue.timestamp * 1000);
|
||||||
|
|
||||||
// Replace in-body timestamps so that they're relative to the cue start time
|
// Replace in-body timestamps so that they're relative to the cue start time
|
||||||
inlineTimestampRegex.lastIndex = 0;
|
inlineTimestampRegex.lastIndex = 0;
|
||||||
@@ -985,7 +987,7 @@ export class MatroskaMuxer extends Muxer {
|
|||||||
|
|
||||||
const subtitleChunk = this.createInternalChunk(
|
const subtitleChunk = this.createInternalChunk(
|
||||||
body,
|
body,
|
||||||
timestamp,
|
cue.timestamp,
|
||||||
cue.duration,
|
cue.duration,
|
||||||
'key',
|
'key',
|
||||||
additions.trim() ? textEncoder.encode(additions) : null,
|
additions.trim() ? textEncoder.encode(additions) : null,
|
||||||
|
|||||||
+8
-17
@@ -81,11 +81,6 @@ export abstract class MediaSource {
|
|||||||
_closingPromise: Promise<void> | null = null;
|
_closingPromise: Promise<void> | null = null;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_closed = false;
|
_closed = false;
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
* A time offset in seconds that is added to all timestamps generated by this source.
|
|
||||||
*/
|
|
||||||
_timestampOffset = 0;
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_ensureValidAdd() {
|
_ensureValidAdd() {
|
||||||
@@ -1476,7 +1471,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
|
|||||||
let frameCount = 0;
|
let frameCount = 0;
|
||||||
let errored = false;
|
let errored = false;
|
||||||
let lastSampleTimestamp: number | null = null;
|
let lastSampleTimestamp: number | null = null;
|
||||||
let pauseOffset = 0;
|
let timestampOffset = 0;
|
||||||
|
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
assert(frameRate !== null);
|
assert(frameRate !== null);
|
||||||
@@ -1544,9 +1539,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
|
|||||||
// pause. Doing it like this instead of simply keeping track of the paused time is better since
|
// pause. Doing it like this instead of simply keeping track of the paused time is better since
|
||||||
// it retains the frame rate of the underlying source.
|
// it retains the frame rate of the underlying source.
|
||||||
const timeDelta = currentTimestamp - lastSampleTimestamp;
|
const timeDelta = currentTimestamp - lastSampleTimestamp;
|
||||||
// We modify this field instead of _timestampOffset since we still might have data in flight
|
timestampOffset -= timeDelta;
|
||||||
// in the encoder, with which we don't want to mess.
|
|
||||||
pauseOffset -= timeDelta;
|
|
||||||
}
|
}
|
||||||
lastSampleTimestamp = currentTimestamp;
|
lastSampleTimestamp = currentTimestamp;
|
||||||
}
|
}
|
||||||
@@ -1574,7 +1567,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._timestampOffset = target - firstVideoFrameTimestamp;
|
timestampOffset = target - firstVideoFrameTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSampleTimestamp = currentTimestamp;
|
lastSampleTimestamp = currentTimestamp;
|
||||||
@@ -1586,7 +1579,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sample = new VideoSample(videoFrame, {
|
const sample = new VideoSample(videoFrame, {
|
||||||
timestamp: currentTimestamp + pauseOffset,
|
timestamp: currentTimestamp + timestampOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
void this._encoder.add(sample, true)
|
void this._encoder.add(sample, true)
|
||||||
@@ -2575,7 +2568,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
|
|||||||
let firstAudioDataTimestamp: number | null = null;
|
let firstAudioDataTimestamp: number | null = null;
|
||||||
let errored = false;
|
let errored = false;
|
||||||
let lastSampleTimestamp: number | null = null;
|
let lastSampleTimestamp: number | null = null;
|
||||||
let pauseOffset = 0;
|
let timestampOffset = 0;
|
||||||
|
|
||||||
const onAudioSample = (audioSample: AudioSample) => {
|
const onAudioSample = (audioSample: AudioSample) => {
|
||||||
if (errored) {
|
if (errored) {
|
||||||
@@ -2593,9 +2586,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
|
|||||||
// the pause. Doing it like this instead of simply keeping track of the paused time is better
|
// the pause. Doing it like this instead of simply keeping track of the paused time is better
|
||||||
// since it retains the sample rate of the underlying source.
|
// since it retains the sample rate of the underlying source.
|
||||||
const timeDelta = currentTimestamp - lastSampleTimestamp;
|
const timeDelta = currentTimestamp - lastSampleTimestamp;
|
||||||
// We modify this field instead of _timestampOffset since we still might have data in flight
|
timestampOffset -= timeDelta;
|
||||||
// in the encoder, with which we don't want to mess.
|
|
||||||
pauseOffset -= timeDelta;
|
|
||||||
}
|
}
|
||||||
lastSampleTimestamp = currentTimestamp;
|
lastSampleTimestamp = currentTimestamp;
|
||||||
}
|
}
|
||||||
@@ -2623,7 +2614,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._timestampOffset = target - firstAudioDataTimestamp;
|
timestampOffset = target - firstAudioDataTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSampleTimestamp = currentTimestamp;
|
lastSampleTimestamp = currentTimestamp;
|
||||||
@@ -2634,7 +2625,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
audioSample.setTimestamp(currentTimestamp + pauseOffset);
|
audioSample.setTimestamp(currentTimestamp + timestampOffset);
|
||||||
|
|
||||||
void this._encoder.add(audioSample, true)
|
void this._encoder.add(audioSample, true)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export class Mp3Muxer extends Muxer {
|
|||||||
this.frameCount++;
|
this.frameCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
this.validateTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||||
|
|
||||||
if (writeXingHeader) {
|
if (writeXingHeader) {
|
||||||
this.framePositions.push(this.writer.getPos());
|
this.framePositions.push(this.writer.getPos());
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export class MpegTsMuxer extends Muxer {
|
|||||||
try {
|
try {
|
||||||
const trackData = this.getVideoTrackData(track, meta);
|
const trackData = this.getVideoTrackData(track, meta);
|
||||||
|
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(
|
this.validateTimestamp(
|
||||||
trackData.track,
|
trackData.track,
|
||||||
packet.timestamp,
|
packet.timestamp,
|
||||||
packet.type === 'key',
|
packet.type === 'key',
|
||||||
@@ -241,7 +241,7 @@ export class MpegTsMuxer extends Muxer {
|
|||||||
|
|
||||||
trackData.timestampProcessingQueue.push({
|
trackData.timestampProcessingQueue.push({
|
||||||
data: preparedData,
|
data: preparedData,
|
||||||
presentationTimestamp: timestamp,
|
presentationTimestamp: packet.timestamp,
|
||||||
decodeTimestamp: null,
|
decodeTimestamp: null,
|
||||||
isKeyframe: packet.type === 'key',
|
isKeyframe: packet.type === 'key',
|
||||||
});
|
});
|
||||||
@@ -260,7 +260,7 @@ export class MpegTsMuxer extends Muxer {
|
|||||||
try {
|
try {
|
||||||
const trackData = this.getAudioTrackData(track, meta);
|
const trackData = this.getAudioTrackData(track, meta);
|
||||||
|
|
||||||
const timestamp = this.validateAndNormalizeTimestamp(
|
this.validateTimestamp(
|
||||||
trackData.track,
|
trackData.track,
|
||||||
packet.timestamp,
|
packet.timestamp,
|
||||||
packet.type === 'key',
|
packet.type === 'key',
|
||||||
@@ -274,7 +274,7 @@ export class MpegTsMuxer extends Muxer {
|
|||||||
|
|
||||||
trackData.timestampProcessingQueue.push({
|
trackData.timestampProcessingQueue.push({
|
||||||
data: preparedData,
|
data: preparedData,
|
||||||
presentationTimestamp: timestamp,
|
presentationTimestamp: packet.timestamp,
|
||||||
decodeTimestamp: null,
|
decodeTimestamp: null,
|
||||||
isKeyframe: packet.type === 'key',
|
isKeyframe: packet.type === 'key',
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-5
@@ -42,9 +42,7 @@ export abstract class Muxer {
|
|||||||
maxTimestampBeforeLastKeyPacket: number | null;
|
maxTimestampBeforeLastKeyPacket: number | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) {
|
protected validateTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) {
|
||||||
timestampInSeconds += track.source._timestampOffset;
|
|
||||||
|
|
||||||
if (timestampInSeconds < 0) {
|
if (timestampInSeconds < 0) {
|
||||||
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
|
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
|
||||||
}
|
}
|
||||||
@@ -78,7 +76,5 @@ export abstract class Muxer {
|
|||||||
|
|
||||||
timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
|
timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return timestampInSeconds;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export class OggMuxer extends Muxer {
|
|||||||
try {
|
try {
|
||||||
const trackData = this.getTrackData(track, meta);
|
const trackData = this.getTrackData(track, meta);
|
||||||
|
|
||||||
this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === 'key');
|
this.validateTimestamp(trackData.track, packet.timestamp, packet.type === 'key');
|
||||||
|
|
||||||
const currentTimestampInSamples = trackData.currentTimestampInSamples;
|
const currentTimestampInSamples = trackData.currentTimestampInSamples;
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export class WaveMuxer extends Muxer {
|
|||||||
this.headerWritten = true;
|
this.headerWritten = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
this.validateTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||||
|
|
||||||
if (!this.isRf64 && this.writer.getPos() + packet.data.byteLength >= 2 ** 32) {
|
if (!this.isRf64 && this.writer.getPos() + packet.data.byteLength >= 2 ** 32) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
Reference in New Issue
Block a user