UNPKG

audio-tracker

Version:

A headless JavaScript library that gives you full control over web audio — playback, tracking, and Media Session integration made simple.

332 lines 13.6 kB
/** * Timestamp module for AudioTracker that tracks audio segments and subsegments in real time. * Fires callbacks on segment, subsegment, and speaker changes based on playback time. * * @param tracker - AudioTracker instance with timestamp options * @returns Cleanup function that unsubscribes from events * * @example * // Example timestamp structure passed during AudioTracker construction: * const timestampData = { * segments: [ * { * id: 'seg1', * start: 0, * end: 30, * order: 1, * speaker: { id: 'sp1', name: 'Speaker 1' }, * label: 'Intro', * text: 'hi everyone' * subSegments: [ * { id: 'sub1', start: 0, end: 10, order: 1, text: 'hi' }, * { id: 'sub2', start: 10, end: 30, order: 2, text: 'everyone' }, * ], * }, * { * id: 'seg2', * start: 30, * end: 60, * order: 2, * label: 'Main section', * text: 'welcome to todays podcast...' * }, * ], * gapBehavior: 'persist-previous', // Optional gap behavior * }; * * const tracker = new AudioTracker('audio.mp3', { timestamp: timestampData }); * tracker.use(timestampModule); */ export function timestampModule(tracker) { // Normalize segments from options, ensuring order and subSegments are set const segments = Array.isArray(tracker.options?.timestamp?.segments) ? tracker.options.timestamp.segments.map((seg, index) => ({ ...seg, subSegments: Array.isArray(seg.subSegments) ? seg.subSegments : [], })) : []; const gapBehavior = tracker.options?.timestamp?.gapBehavior || null; if (!segments.length) { console.warn("TimestampModule: No segments provided or array is empty."); return () => { }; } // Removed runtime type checks for seg.start and seg.end because TS guarantees their type const validatedSegments = segments .filter((seg) => { if (seg.start >= seg.end) { console.warn("TimestampModule: Segment start >= end", seg); return false; } return true; }) .sort((a, b) => a.start - b.start); if (validatedSegments.length === 0) { console.warn("TimestampModule: No valid segments after validation."); return () => { }; } let currentSegmentIndex = -1; let currentSubSegmentIndex = -1; let currentSpeaker = null; let lastValidSegmentIndex = -1; let lastReportedSegment = null; let lastReportedSubSegment = null; let isInitialized = false; // Removed typeof and isNaN checks on 'time' parameter; TS type system covers this function findSegmentIndexByTime(time) { return validatedSegments.findIndex((seg) => time >= seg.start && time < seg.end); } function findSubSegmentIndexByTime(segment, time) { if (!segment.subSegments?.length) return -1; return segment.subSegments.findIndex((sub) => time >= sub.start && time < sub.end); } function findPreviousSegmentByTime(time) { for (let i = validatedSegments.length - 1; i >= 0; i--) { if (validatedSegments[i].end <= time) return i; } return -1; } function findNextSegmentByTime(time) { for (let i = 0; i < validatedSegments.length; i++) { if (validatedSegments[i].start >= time) return i; } return -1; } function getSegmentWithGapBehavior(segmentIndex, time) { if (segmentIndex !== -1) return validatedSegments[segmentIndex]; if (gapBehavior === "persist-previous") { const prevIndex = findPreviousSegmentByTime(time); if (prevIndex !== -1) { lastValidSegmentIndex = prevIndex; return validatedSegments[prevIndex]; } return lastValidSegmentIndex !== -1 ? validatedSegments[lastValidSegmentIndex] : null; } else if (gapBehavior === "persist-next") { const nextIndex = findNextSegmentByTime(time); return nextIndex !== -1 ? validatedSegments[nextIndex] : null; } return null; } function getSubSegmentWithGapBehavior(time) { if (currentSegmentIndex !== -1 && currentSubSegmentIndex !== -1 && Array.isArray(validatedSegments[currentSegmentIndex].subSegments)) { return (validatedSegments[currentSegmentIndex].subSegments?.[currentSubSegmentIndex] ?? null); } if (gapBehavior === "persist-previous" && lastValidSegmentIndex !== -1 && Array.isArray(validatedSegments[lastValidSegmentIndex].subSegments) && (validatedSegments[lastValidSegmentIndex].subSegments?.length ?? 0) > 0) { const seg = validatedSegments[lastValidSegmentIndex]; return seg.subSegments?.[seg.subSegments.length - 1] ?? null; } else if (gapBehavior === "persist-next") { const nextIndex = findNextSegmentByTime(time); if (nextIndex !== -1 && Array.isArray(validatedSegments[nextIndex].subSegments) && (validatedSegments[nextIndex].subSegments?.length ?? 0) > 0) { const seg = validatedSegments[nextIndex]; return seg.subSegments?.[0] ?? null; } } return null; } function handleTimeUpdate() { const currentTime = tracker.getCurrentTime(); if (currentTime < 0) return; const newSegmentIndex = findSegmentIndexByTime(currentTime); const segmentToReport = getSegmentWithGapBehavior(newSegmentIndex, currentTime); const segmentChanged = newSegmentIndex !== currentSegmentIndex || !isInitialized || lastReportedSegment?.id !== segmentToReport?.id; if (segmentChanged) { if (lastReportedSegment !== null) tracker.callbacks.onSegmentExit?.(lastReportedSegment); currentSegmentIndex = newSegmentIndex; if (currentSegmentIndex !== -1) lastValidSegmentIndex = currentSegmentIndex; if (segmentToReport !== null) { tracker.callbacks.onSegmentChange?.(segmentToReport); tracker.callbacks.onSegmentEnter?.(segmentToReport); lastReportedSegment = segmentToReport; const newSpeaker = segmentToReport.speaker ?? null; if (currentSpeaker?.id !== newSpeaker?.id) { currentSpeaker = newSpeaker; tracker.callbacks.onSpeakerChange?.(currentSpeaker); } } else { tracker.callbacks.onSegmentChange?.(null); lastReportedSegment = null; if (currentSpeaker !== null) { currentSpeaker = null; tracker.callbacks.onSpeakerChange?.(null); } } } const subSegmentToReport = getSubSegmentWithGapBehavior(currentTime); if (currentSegmentIndex !== -1) { const currentSegment = validatedSegments[currentSegmentIndex]; const newSubSegmentIndex = findSubSegmentIndexByTime(currentSegment, currentTime); if (newSubSegmentIndex !== currentSubSegmentIndex) { if (lastReportedSubSegment !== null) tracker.callbacks.onSubSegmentExit?.(lastReportedSubSegment); currentSubSegmentIndex = newSubSegmentIndex; if (currentSubSegmentIndex !== -1 && Array.isArray(currentSegment.subSegments)) { const subSeg = currentSegment.subSegments[currentSubSegmentIndex]; tracker.callbacks.onSubSegmentChange?.(subSeg); tracker.callbacks.onSubSegmentEnter?.(subSeg); lastReportedSubSegment = subSeg; } else { tracker.callbacks.onSubSegmentChange?.(null); lastReportedSubSegment = null; } } } else { const subSegChanged = lastReportedSubSegment?.id !== subSegmentToReport?.id; if (subSegChanged) { if (subSegmentToReport !== null) { tracker.callbacks.onSubSegmentChange?.(subSegmentToReport); lastReportedSubSegment = subSegmentToReport; } else { if (currentSubSegmentIndex !== -1) { currentSubSegmentIndex = -1; tracker.callbacks.onSubSegmentChange?.(null); lastReportedSubSegment = null; } } } } isInitialized = true; } function handleSeeking() { currentSegmentIndex = -1; isInitialized = false; handleTimeUpdate(); } handleTimeUpdate(); tracker.getAllSegments = () => validatedSegments; tracker.getSubSegmentsBySegmentId = (id) => { const segment = validatedSegments.find((seg) => seg.id === id); return Array.isArray(segment?.subSegments) ? segment.subSegments : []; }; tracker.getNextSegment = () => { if (currentSegmentIndex === -1) return null; return validatedSegments[currentSegmentIndex + 1] || null; }; tracker.getPreviousSegment = () => { if (currentSegmentIndex <= 0) return null; return validatedSegments[currentSegmentIndex - 1] || null; }; tracker.seekToNextSegment = () => { const nextSegment = tracker.getNextSegment ? tracker.getNextSegment() : null; if (nextSegment?.start != null) { const duration = tracker.getDuration(); const seekTime = Math.min(Math.max(nextSegment.start, 0), duration); tracker.seekTo(seekTime); } }; tracker.seekToPreviousSegment = () => { const prevSegment = tracker.getPreviousSegment ? tracker.getPreviousSegment() : null; if (prevSegment?.start != null) { const duration = tracker.getDuration(); const seekTime = Math.min(Math.max(prevSegment.start, 0), duration); tracker.seekTo(seekTime); } }; tracker.isInGap = () => { const currentTime = tracker.getCurrentTime(); return findSegmentIndexByTime(currentTime) === -1; }; tracker.getGapBehavior = () => gapBehavior; tracker.getSegmentAtTime = (time) => { const segIndex = findSegmentIndexByTime(time); return getSegmentWithGapBehavior(segIndex, time); }; tracker.getSubSegmentAtTime = (time) => { const segAtTime = tracker.getSegmentAtTime ? tracker.getSegmentAtTime(time) : null; if (!segAtTime) return null; const subIndex = findSubSegmentIndexByTime(segAtTime, time); if (Array.isArray(segAtTime.subSegments) && subIndex !== -1) return segAtTime.subSegments[subIndex] || null; if (gapBehavior === "persist-previous" && Array.isArray(segAtTime.subSegments) && segAtTime.subSegments.length) { return segAtTime.subSegments[segAtTime.subSegments.length - 1]; } else if (gapBehavior === "persist-next" && Array.isArray(segAtTime.subSegments) && segAtTime.subSegments.length) { return segAtTime.subSegments[0] || null; } return null; }; tracker.getCurrentSegment = () => { const currentTime = tracker.getCurrentTime(); if (currentSegmentIndex !== -1) return validatedSegments[currentSegmentIndex]; return getSegmentWithGapBehavior(currentSegmentIndex, currentTime); }; tracker.getCurrentSubSegment = () => { const currentTime = tracker.getCurrentTime(); return getSubSegmentWithGapBehavior(currentTime); }; tracker.getCurrentSpeaker = () => { const currentSeg = tracker.getCurrentSegment ? tracker.getCurrentSegment() : null; return currentSeg?.speaker || null; }; tracker.seekToSegmentById = (id) => { if (!id) return; const segment = validatedSegments.find((seg) => seg.id === id); if (segment?.start != null) { const duration = tracker.getDuration(); const seekTime = Math.min(Math.max(segment.start, 0), duration); tracker.seekTo(seekTime); } }; tracker.seekToSubSegmentById = (id) => { if (!id) return; for (const segment of validatedSegments) { if (Array.isArray(segment.subSegments) && segment.subSegments.length) { const sub = segment.subSegments.find((s) => s.id === id); if (sub?.start != null) { const duration = tracker.getDuration(); const seekTime = Math.min(Math.max(sub.start, 0), duration); tracker.seekTo(seekTime); return; } } } }; tracker.subscribe("timeupdate", handleTimeUpdate); tracker.subscribe("seeking", handleSeeking); return () => { tracker.unsubscribe("timeupdate", handleTimeUpdate); tracker.unsubscribe("seeking", handleSeeking); }; } //# sourceMappingURL=timestampModule.js.map