Files
Recordly/src/lib/mediaTiming.ts
T

196 lines
5.3 KiB
TypeScript

export function clampMediaTimeToDuration(targetTime: number, duration?: number | null): number {
const safeTargetTime = Math.max(0, targetTime);
if (!Number.isFinite(duration) || duration === null || duration === undefined) {
return safeTargetTime;
}
return Math.max(0, Math.min(safeTargetTime, Math.max(0, duration)));
}
const MIN_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS = 0.5;
const MAX_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS = 1;
const PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_RATIO = 0.01;
export function resolvePreviewMediaDuration(
reportedDuration?: number | null,
timelineDuration?: number | null,
): number | null {
if (!Number.isFinite(reportedDuration)) {
return null;
}
const safeReportedDuration = Math.max(0, reportedDuration ?? 0);
if (
!Number.isFinite(timelineDuration) ||
timelineDuration === null ||
timelineDuration === undefined
) {
return safeReportedDuration;
}
const safeTimelineDuration = Math.max(0, timelineDuration);
if (safeTimelineDuration === 0) {
return safeReportedDuration;
}
const shortfallToleranceSeconds = Math.min(
MAX_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS,
Math.max(
MIN_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS,
safeTimelineDuration * PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_RATIO,
),
);
if (safeTimelineDuration - safeReportedDuration > shortfallToleranceSeconds) {
return null;
}
return safeReportedDuration;
}
const MIN_COMPANION_AUDIO_DELAY_SECONDS = 0.025;
const MAX_INFERRED_COMPANION_AUDIO_DELAY_SECONDS = 0.5;
export function estimateCompanionAudioStartDelaySeconds(
timelineDuration?: number | null,
audioDuration?: number | null,
recordedStartDelayMs?: number | null,
): number {
if (Number.isFinite(recordedStartDelayMs) && (recordedStartDelayMs ?? 0) >= 0) {
return Math.max(0, recordedStartDelayMs ?? 0) / 1000;
}
if (!Number.isFinite(timelineDuration) || !Number.isFinite(audioDuration)) {
return 0;
}
const safeTimelineDuration = Math.max(0, timelineDuration ?? 0);
const safeAudioDuration = Math.max(0, audioDuration ?? 0);
const estimatedDelaySeconds = safeTimelineDuration - safeAudioDuration;
if (
estimatedDelaySeconds <= MIN_COMPANION_AUDIO_DELAY_SECONDS ||
estimatedDelaySeconds > MAX_INFERRED_COMPANION_AUDIO_DELAY_SECONDS
) {
return 0;
}
return estimatedDelaySeconds;
}
export function getMediaSyncPlaybackRate({
basePlaybackRate,
currentTime,
targetTime,
toleranceSeconds = 0.015,
correctionWindowSeconds = 2,
maxAdjustment = 0.08,
}: {
basePlaybackRate: number;
currentTime: number;
targetTime: number;
toleranceSeconds?: number;
correctionWindowSeconds?: number;
maxAdjustment?: number;
}): number {
const safeBasePlaybackRate =
Number.isFinite(basePlaybackRate) && basePlaybackRate > 0 ? basePlaybackRate : 1;
if (!Number.isFinite(currentTime) || !Number.isFinite(targetTime)) {
return safeBasePlaybackRate;
}
const driftSeconds = targetTime - currentTime;
if (Math.abs(driftSeconds) <= toleranceSeconds) {
return safeBasePlaybackRate;
}
const safeCorrectionWindow = correctionWindowSeconds > 0 ? correctionWindowSeconds : 2;
const safeMaxAdjustment = Math.max(0, maxAdjustment);
const adjustment = Math.max(
-safeMaxAdjustment,
Math.min(safeMaxAdjustment, driftSeconds / safeCorrectionWindow),
);
return Math.max(0.1, safeBasePlaybackRate + adjustment);
}
type PitchPreservingMediaElement = HTMLMediaElement & {
preservesPitch?: boolean;
mozPreservesPitch?: boolean;
webkitPreservesPitch?: boolean;
};
export function enablePitchPreservingPlayback(media: HTMLMediaElement) {
const pitchMedia = media as PitchPreservingMediaElement;
pitchMedia.preservesPitch = true;
pitchMedia.mozPreservesPitch = true;
pitchMedia.webkitPreservesPitch = true;
}
export function getEffectiveVideoStreamDurationSeconds({
duration,
streamDuration,
}: {
duration?: number | null;
streamDuration?: number | null;
}): number {
const safeDuration =
Number.isFinite(duration) && (duration ?? 0) > 0 ? Math.max(0, duration ?? 0) : 0;
const safeStreamDuration =
Number.isFinite(streamDuration) && (streamDuration ?? 0) > 0
? Math.max(0, streamDuration ?? 0)
: 0;
if (safeDuration > 0 && safeStreamDuration > 0) {
const gapSeconds = safeDuration - safeStreamDuration;
const largeMismatchThresholdSeconds = Math.max(2, safeDuration * 0.1);
if (gapSeconds > largeMismatchThresholdSeconds) {
return safeDuration;
}
return safeStreamDuration;
}
if (safeStreamDuration > 0) {
return safeStreamDuration;
}
if (safeDuration > 0) {
return safeDuration;
}
return 0;
}
export function getEffectiveRecordingDurationMs({
startTimeMs,
endTimeMs,
accumulatedPausedDurationMs = 0,
pauseStartedAtMs = null,
}: {
startTimeMs: number;
endTimeMs: number;
accumulatedPausedDurationMs?: number;
pauseStartedAtMs?: number | null;
}): number {
if (!Number.isFinite(startTimeMs) || !Number.isFinite(endTimeMs)) {
return 0;
}
const safeStartTime = Math.max(0, startTimeMs);
const safeEndTime = Math.max(safeStartTime, endTimeMs);
const activePauseDuration =
Number.isFinite(pauseStartedAtMs) && pauseStartedAtMs !== null
? Math.max(0, safeEndTime - pauseStartedAtMs)
: 0;
return Math.max(
0,
safeEndTime -
safeStartTime -
Math.max(0, accumulatedPausedDurationMs) -
activePauseDuration,
);
}