mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-10-04 03:27:56 +00:00
196 lines
5.3 KiB
TypeScript
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,
|
|
);
|
|
}
|