@memberjunction/ng-media-player 0.0.1 → 5.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +241 -28
  2. package/dist/lib/media-player/cue-utils.d.ts +24 -0
  3. package/dist/lib/media-player/cue-utils.d.ts.map +1 -0
  4. package/dist/lib/media-player/cue-utils.js +47 -0
  5. package/dist/lib/media-player/cue-utils.js.map +1 -0
  6. package/dist/lib/media-player/media-player.component.d.ts +326 -0
  7. package/dist/lib/media-player/media-player.component.d.ts.map +1 -0
  8. package/dist/lib/media-player/media-player.component.js +1545 -0
  9. package/dist/lib/media-player/media-player.component.js.map +1 -0
  10. package/dist/lib/media-player/playback-state.d.ts +46 -0
  11. package/dist/lib/media-player/playback-state.d.ts.map +1 -0
  12. package/dist/lib/media-player/playback-state.js +86 -0
  13. package/dist/lib/media-player/playback-state.js.map +1 -0
  14. package/dist/lib/media-player/transcript-layout.d.ts +35 -0
  15. package/dist/lib/media-player/transcript-layout.d.ts.map +1 -0
  16. package/dist/lib/media-player/transcript-layout.js +37 -0
  17. package/dist/lib/media-player/transcript-layout.js.map +1 -0
  18. package/dist/lib/media-player/waveform-utils.d.ts +45 -0
  19. package/dist/lib/media-player/waveform-utils.d.ts.map +1 -0
  20. package/dist/lib/media-player/waveform-utils.js +108 -0
  21. package/dist/lib/media-player/waveform-utils.js.map +1 -0
  22. package/dist/lib/media-player.types.d.ts +107 -0
  23. package/dist/lib/media-player.types.d.ts.map +1 -0
  24. package/dist/lib/media-player.types.js +9 -0
  25. package/dist/lib/media-player.types.js.map +1 -0
  26. package/dist/lib/storage-media-player/storage-media-player.component.d.ts +77 -0
  27. package/dist/lib/storage-media-player/storage-media-player.component.d.ts.map +1 -0
  28. package/dist/lib/storage-media-player/storage-media-player.component.js +283 -0
  29. package/dist/lib/storage-media-player/storage-media-player.component.js.map +1 -0
  30. package/dist/public-api.d.ts +8 -0
  31. package/dist/public-api.d.ts.map +1 -0
  32. package/dist/public-api.js +11 -0
  33. package/dist/public-api.js.map +1 -0
  34. package/package.json +36 -7
package/README.md CHANGED
@@ -1,45 +1,258 @@
1
1
  # @memberjunction/ng-media-player
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ A world-class, zero-dependency Angular media (audio/video) player for MemberJunction applications, plus an MJStorage-bound wrapper that streams files stored in any MJ storage provider. The generic player has **no MemberJunction-core dependency** — it is pure Angular and safe to reuse in any app; all the MJ wiring lives in the separate wrapper.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Installation
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ```bash
8
+ npm install @memberjunction/ng-media-player
9
+ ```
8
10
 
9
- ## Purpose
11
+ ## Overview
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/ng-media-player`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ This package ships **two** standalone components:
15
14
 
16
- ## What is OIDC Trusted Publishing?
15
+ | Component | Selector | Purpose |
16
+ |-----------|----------|---------|
17
+ | `MJMediaPlayerComponent` | `mj-media-player` | Generic, framework-agnostic player. You give it one or more `MediaTrack`s (plain URLs) and an optional transcript. |
18
+ | `MJStorageMediaPlayerComponent` | `mj-storage-media-player` | MJStorage-bound wrapper. You give it an `MJ: Files` `FileID` (or several); it resolves each to a short-lived authenticated streaming URL and hands them to the generic player. |
17
19
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
20
+ The generic player renders a custom transport bar (play/pause, click-and-drag scrubber, playback-rate menu, ±skip, volume + mute, fullscreen), a **real audio waveform** that doubles as the scrubber, and an optional **time-synced transcript** whose cues are clickable and auto-highlight as playback advances. For a single audio track it shows the transport + waveform; for multiple video tracks it renders a responsive grid.
19
21
 
20
- ## Setup Instructions
22
+ ```mermaid
23
+ flowchart TD
24
+ subgraph Wrapper["MJStorageMediaPlayerComponent (mj-storage-media-player)"]
25
+ F["FileID / FileIDs[]"] --> M["CreateMediaAccessToken mutation<br/>(per-user permission check)"]
26
+ M --> U["/media/:fileId?token= streaming URL<br/>+ optional peaks.json peaks"]
27
+ end
28
+ subgraph Generic["MJMediaPlayerComponent (mj-media-player)"]
29
+ T["MediaTrack[]"] --> P["Transport bar + waveform scrubber"]
30
+ T --> W["Real waveform (supplied Peaks OR client-decoded)"]
31
+ C["MediaTranscriptCue[]"] --> X["Click-to-seek transcript (auto-highlight)"]
32
+ end
33
+ U -->|"MediaTrack[] (Url + Peaks)"| Generic
21
34
 
22
- To properly configure OIDC trusted publishing for this package:
35
+ style Wrapper fill:#2d6a9f,stroke:#1a4971,color:#fff
36
+ style Generic fill:#2d8659,stroke:#1a5c3a,color:#fff
37
+ ```
23
38
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
39
+ ## Key features
28
40
 
29
- ## DO NOT USE THIS PACKAGE
41
+ - **Custom transport** — play/pause, click-and-drag scrubber, playback-rate menu, ±skip (configurable seconds), volume slider + mute, fullscreen for video.
42
+ - **Real audio waveform** — for audio-only tracks the player renders a true waveform (not a synthetic one) that **IS** the scrubber. Peaks are either supplied via `MediaTrack.Peaks` (rendered as-is, no decode) or extracted client-side by decoding the track URL through an `AudioContext`. On any decode failure it falls back gracefully to a plain progress bar.
43
+ - **Time-synced transcript** — supply `MediaTranscriptCue[]` and the player renders a transcript panel; clicking a cue seeks to its start, and the active cue auto-highlights as playback advances. Defaults to **below** the player (`TranscriptPosition='bottom'`) with a show/hide toggle in the transport bar.
44
+ - **Loading / buffering state** — a `MediaPlaybackState` lifecycle (`idle`/`loading`/`buffering`/`ready`/`playing`/`paused`/`ended`/`error`) drives a stage busy-indicator overlay (the buffering spinner is debounced so instant seeks don't flash it) and a screen-reader-friendly status label.
45
+ - **Multi-track video grid** — multiple video tracks render in a responsive grid; the first video leads the master timeline.
46
+ - **Keyboard support** — space (play/pause), ←/→ (±5s), J/L (±10s), ↑/↓ (volume), M (mute), F (fullscreen); the scrubber itself is a `role="slider"` with arrow/Home/End support.
47
+ - **Cancelable `Before*` events** — `BeforePlay`/`BeforePause`/`BeforeSeek`/`BeforeRateChange` each pass an event object; set `event.Cancel = true` to abort the pending action (mirrors the ng-conversations `beforeAgentTurn` pattern).
48
+ - **Imperative API** — `Play()`, `Pause()`, `SeekToMs()`, `SetPlaybackRate()`, `SeekToCue()`, etc.
49
+ - **Reduced-motion aware** — suppresses the waveform draw-in animation when the user prefers reduced motion.
30
50
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
51
+ ## Usage
36
52
 
37
- ## More Information
53
+ ### Generic player (plain URLs)
38
54
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
55
+ ```typescript
56
+ import { MJMediaPlayerComponent, MediaTrack, MediaTranscriptCue } from '@memberjunction/ng-media-player';
42
57
 
43
- ---
58
+ @Component({
59
+ standalone: true,
60
+ imports: [MJMediaPlayerComponent],
61
+ template: `
62
+ <mj-media-player
63
+ [Tracks]="tracks"
64
+ [Transcript]="transcript"
65
+ (Ended)="onEnded()">
66
+ </mj-media-player>
67
+ `,
68
+ })
69
+ export class MyComponent {
70
+ tracks: MediaTrack[] = [
71
+ { Id: 'a1', Kind: 'audio', Url: 'https://example.com/interview.wav' },
72
+ ];
73
+ transcript: MediaTranscriptCue[] = [
74
+ { Id: 'c1', StartMs: 0, SpeakerLabel: 'Host', Text: 'Welcome to the show.' },
75
+ { Id: 'c2', StartMs: 3200, SpeakerLabel: 'Guest', Text: 'Thanks for having me.' },
76
+ ];
77
+ onEnded() { /* ... */ }
78
+ }
79
+ ```
44
80
 
45
- **Maintained for OIDC setup purposes only**
81
+ ### Storage-backed player (MJ: Files)
82
+
83
+ ```typescript
84
+ import { MJStorageMediaPlayerComponent } from '@memberjunction/ng-media-player';
85
+
86
+ @Component({
87
+ standalone: true,
88
+ imports: [MJStorageMediaPlayerComponent],
89
+ template: `
90
+ <mj-storage-media-player
91
+ [FileID]="recordingFileId"
92
+ [Transcript]="transcript">
93
+ </mj-storage-media-player>
94
+ `,
95
+ })
96
+ export class RecordingPlaybackComponent {
97
+ recordingFileId = '...'; // an MJ: Files row ID
98
+ transcript: MediaTranscriptCue[] = [ /* ... */ ];
99
+ }
100
+ ```
101
+
102
+ The wrapper resolves the file id to a streaming URL on init (and whenever `FileID`/`FileIDs` change), surfacing graceful loading / no-access / empty states while it does.
103
+
104
+ ## `MJMediaPlayerComponent` (`mj-media-player`)
105
+
106
+ ### Inputs
107
+
108
+ | Property | Type | Default | Description |
109
+ |----------|------|---------|-------------|
110
+ | `Tracks` | `MediaTrack[]` | `[]` | The track(s) to play. One audio track → transport bar + waveform; many videos → grid. |
111
+ | `Transcript` | `MediaTranscriptCue[] \| null` | `null` | Transcript cues. When set (and `ShowTranscript`), a transcript panel renders. |
112
+ | `Autoplay` | `boolean` | `false` | Begin playback automatically once media is ready. |
113
+ | `StartAtMs` | `number \| null` | `null` | Start playback at this position (ms) once metadata loads. |
114
+ | `ShowTranscript` | `boolean` | `true` | Master switch for the transcript panel (when a transcript is provided). |
115
+ | `ShowTranscriptToggle` | `boolean` | `true` | Whether the transcript show/hide toggle renders in the transport bar. |
116
+ | `ShowSpeedControl` | `boolean` | `true` | Whether the playback-rate menu is shown. |
117
+ | `ShowSkipControls` | `boolean` | `true` | Whether the ±skip buttons are shown. |
118
+ | `ShowVolume` | `boolean` | `true` | Whether the volume slider + mute are shown. |
119
+ | `ShowWaveform` | `boolean` | `true` | Whether the real audio waveform (which doubles as the scrubber) is shown for audio-only tracks. |
120
+ | `WaveformBarCount` | `number` | `DEFAULT_WAVEFORM_BARS` | Number of bars the waveform renders / downsamples to. |
121
+ | `ShowFullscreen` | `boolean` | `true` | Whether the fullscreen button is shown for video. |
122
+ | `SkipSeconds` | `number` | `30` | Seconds the ±skip buttons jump. |
123
+ | `PlaybackRates` | `number[]` | `[0.5, 1, 1.25, 1.5, 2]` | Rates offered in the rate menu. |
124
+ | `InitialRate` | `number` | `1` | Initial playback rate. |
125
+ | `InitialVolume` | `number` | `1` | Initial volume (0..1). |
126
+ | `TranscriptPosition` | `'bottom' \| 'side'` | `'bottom'` | Where the transcript panel sits relative to the media. |
127
+
128
+ ### Outputs
129
+
130
+ `Before*` events are **cancelable** — set `event.Cancel = true` in a handler to abort.
131
+
132
+ | Event | Type | Description |
133
+ |-------|------|-------------|
134
+ | `BeforePlay` | `EventEmitter<MediaPlayerCancelableEvent>` | Before playback starts (cancelable). |
135
+ | `AfterPlay` | `EventEmitter<void>` | After playback starts. |
136
+ | `BeforePause` | `EventEmitter<MediaPlayerCancelableEvent>` | Before pause (cancelable). |
137
+ | `AfterPause` | `EventEmitter<void>` | After pause. |
138
+ | `BeforeSeek` | `EventEmitter<MediaSeekEvent>` | Before a seek (cancelable; carries `FromMs`/`ToMs`). |
139
+ | `AfterSeek` | `EventEmitter<number>` | After a seek (the new position in ms). |
140
+ | `BeforeRateChange` | `EventEmitter<MediaRateEvent>` | Before a rate change (cancelable; carries `FromRate`/`ToRate`). |
141
+ | `AfterRateChange` | `EventEmitter<number>` | After a rate change (the new rate). |
142
+ | `CueActivated` | `EventEmitter<MediaCueEvent>` | When the active transcript cue changes during playback. |
143
+ | `CueClicked` | `EventEmitter<MediaCueEvent>` | When a transcript cue is clicked. |
144
+ | `TimeUpdate` | `EventEmitter<number>` | On every time update (current position in ms). |
145
+ | `DurationChange` | `EventEmitter<number>` | When the media duration is resolved/changes (ms). |
146
+ | `Ended` | `EventEmitter<void>` | When playback reaches the end. |
147
+ | `StateChanged` | `EventEmitter<MediaPlaybackState>` | On every playback-lifecycle transition. |
148
+ | `TranscriptVisibilityChanged` | `EventEmitter<boolean>` | When the transcript is toggled (`true` = shown). |
149
+
150
+ ### Methods (imperative API)
151
+
152
+ | Method | Returns | Description |
153
+ |--------|---------|-------------|
154
+ | `Play()` | `void` | Begin playback (honors `BeforePlay`). |
155
+ | `Pause()` | `void` | Pause playback (honors `BeforePause`). |
156
+ | `TogglePlay()` | `void` | Toggle play/pause. |
157
+ | `SeekToMs(ms)` | `void` | Seek to an absolute position (honors `BeforeSeek`). |
158
+ | `SkipForward()` / `SkipBackward()` | `void` | Skip ±`SkipSeconds`. |
159
+ | `SetPlaybackRate(rate)` | `void` | Set the playback rate (honors `BeforeRateChange`). |
160
+ | `SeekToCue(index)` | `void` | Seek to a transcript cue's start (continues playing if playing, repositions if paused). |
161
+ | `SetVolume(v)` | `void` | Set volume 0..1 (unmutes if > 0). |
162
+ | `ToggleMute()` | `void` | Toggle mute. |
163
+ | `EnterFullscreen()` / `ExitFullscreen()` / `ToggleFullscreen()` | `void` | Fullscreen control for video. |
164
+ | `ToggleTranscript()` | `void` | Show/hide the transcript panel (emits `TranscriptVisibilityChanged`). |
165
+
166
+ Read-only getters expose live state for template binding / inspection, including `CurrentTimeMs`, `DurationMs`, `IsPlaying`, `PlaybackRate`, `ActiveCueIndex`, `MediaState`, `IsLoading`, `IsBuffering`, and `StatusLabel`.
167
+
168
+ ## `MJStorageMediaPlayerComponent` (`mj-storage-media-player`)
169
+
170
+ Extends `BaseAngularComponent`, so it accepts the standard `[Provider]` input for multi-provider apps.
171
+
172
+ ### Inputs
173
+
174
+ | Property | Type | Default | Description |
175
+ |----------|------|---------|-------------|
176
+ | `FileID` | `string \| null` | `null` | Convenience single `MJ: Files` id. Combined with `FileIDs`; duplicates are de-duped. |
177
+ | `FileIDs` | `string[]` | `[]` | Multi-track file ids (e.g. separate meeting streams). |
178
+ | `Transcript` | `MediaTranscriptCue[] \| null` | `null` | Transcript cues forwarded to the generic player. |
179
+ | `ShowTranscript` / `ShowTranscriptToggle` / `ShowSpeedControl` / `ShowSkipControls` / `ShowVolume` / `ShowWaveform` / `ShowFullscreen` | `boolean` | `true` | Pass-through feature flags, forwarded to the generic player. |
180
+ | `SkipSeconds` | `number` | `30` | Forwarded skip step. |
181
+ | `PlaybackRates` | `number[]` | `[0.5, 1, 1.25, 1.5, 2]` | Forwarded rate menu. |
182
+ | `Autoplay` | `boolean` | `false` | Forwarded autoplay flag. |
183
+ | `StartAtMs` | `number \| null` | `null` | Forwarded start position. |
184
+ | `TranscriptPosition` | `'side' \| 'bottom'` | `'bottom'` | Forwarded transcript position. |
185
+
186
+ ### Read-only getters
187
+
188
+ | Getter | Type | Description |
189
+ |--------|------|-------------|
190
+ | `ResolvedTracks` | `MediaTrack[]` | The streaming tracks resolved from the file ids. |
191
+ | `IsLoading` | `boolean` | True while minting access tokens / resolving tracks. |
192
+ | `AccessError` | `string \| null` | A no-access / unavailable message, set only when **nothing** resolved. |
193
+ | `HasFileIds` | `boolean` | Whether any file id is configured. |
194
+
195
+ ## Streaming & permission model (wrapper)
196
+
197
+ The wrapper resolves each file id by calling the **`CreateMediaAccessToken`** GraphQL mutation (server-side, in `@memberjunction/server`). The server:
198
+
199
+ 1. Runs a **per-user permission check** for the file.
200
+ 2. Mints a short-lived (HS256) signed token and returns the authenticated streaming URL `GET /media/:fileId?token=…`.
201
+ 3. If a `peaks.json` sidecar sits beside the recording in storage, returns its precomputed waveform **peaks** alongside the URL.
202
+
203
+ The `<audio>` / `<video>` element then streams that URL natively over **HTTP Range** (progressive playback + seek-before-download for large video) instead of base64'ing the whole file over GraphQL. The `/media` route re-verifies the token's signature/expiry and that it matches the requested file, then range-streams the bytes from the underlying MJ storage provider via `FileStorageBase.GetObjectStream` (see [@memberjunction/storage](../../../MJStorage/docs/STREAMING.md)).
204
+
205
+ **Waveform peaks:** when the server supplies precomputed peaks (from the `peaks.json` sidecar), the wrapper sets them on `MediaTrack.Peaks` so the generic player renders the real waveform **instantly with no client-side fetch/decode**. When no sidecar exists, audio waveforms still decode client-side from the streaming URL; video shows the plain progress bar.
206
+
207
+ ## Public types
208
+
209
+ ```typescript
210
+ type MediaKind = 'audio' | 'video';
211
+
212
+ type MediaPlaybackState =
213
+ | 'idle' | 'loading' | 'buffering' | 'ready'
214
+ | 'playing' | 'paused' | 'ended' | 'error';
215
+
216
+ interface MediaTrack {
217
+ Id: string; // stable identifier (track-by + event correlation)
218
+ Kind: MediaKind; // audio vs. video
219
+ Url: string; // http(s) or blob: object URL
220
+ MimeType?: string; // optional MIME hint (e.g. 'audio/wav', 'video/mp4')
221
+ Label?: string; // optional label shown in multi-track layouts
222
+ PosterUrl?: string; // optional poster image for video
223
+ Peaks?: number[]; // optional precomputed waveform peaks (normalized 0..1, one per bar).
224
+ // Supplied → rendered directly, NO client-side decode.
225
+ // Absent (audio + waveform on) → player decodes the URL client-side.
226
+ }
227
+
228
+ interface MediaTranscriptCue {
229
+ Id: string; // stable cue identifier
230
+ StartMs: number; // cue start (ms from media start)
231
+ EndMs?: number; // optional cue end; when absent, the next cue's start is used
232
+ SpeakerLabel?: string; // optional speaker label (color-coded)
233
+ Text: string; // the spoken text
234
+ }
235
+
236
+ interface MediaPlayerCancelableEvent { Cancel: boolean; } // set true to abort
237
+ interface MediaSeekEvent extends MediaPlayerCancelableEvent { FromMs: number; ToMs: number; }
238
+ interface MediaRateEvent extends MediaPlayerCancelableEvent { FromRate: number; ToRate: number; }
239
+ interface MediaCueEvent { Cue: MediaTranscriptCue; Index: number; }
240
+ ```
241
+
242
+ Helper utilities (`computeActiveCueIndex`, `downsamplePeaks`, `nextPlaybackState`, transcript-layout resolvers) are also exported from the package public API for reuse and testing.
243
+
244
+ ## Dependencies
245
+
246
+ - `@angular/common` / `@angular/core` (peer)
247
+ - `@memberjunction/core`, `@memberjunction/graphql-dataprovider`, `@memberjunction/ng-base-types` — used **only** by the storage wrapper. The generic `MJMediaPlayerComponent` has no MemberJunction dependency.
248
+
249
+ ## Building & testing
250
+
251
+ ```bash
252
+ npm run build # ngc
253
+ npm run test # vitest
254
+ ```
255
+
256
+ ## License
257
+
258
+ ISC
@@ -0,0 +1,24 @@
1
+ import { MediaTranscriptCue } from '../media-player.types';
2
+ /**
3
+ * Pure computation of the active transcript cue for a given playback position.
4
+ *
5
+ * A cue is active when `currentMs` falls within `[StartMs, end)` where `end` is:
6
+ * - the cue's own `EndMs` when present, OR
7
+ * - the next cue's `StartMs` (the cue runs until the next one begins), OR
8
+ * - `+Infinity` for the final cue with no `EndMs` (runs to the end of the media).
9
+ *
10
+ * Cues are assumed to be in chronological order by `StartMs`. When more than one
11
+ * cue qualifies (overlapping ranges), the LAST qualifying cue wins, which matches
12
+ * the "most recently started" intuition for transcripts.
13
+ *
14
+ * @param currentMs current playback position in milliseconds
15
+ * @param cues the ordered transcript cues
16
+ * @returns the index of the active cue, or -1 if none is active (e.g. before the first cue)
17
+ */
18
+ export declare function computeActiveCueIndex(currentMs: number, cues: MediaTranscriptCue[] | null | undefined): number;
19
+ /**
20
+ * Resolves the effective end time (exclusive) of the cue at `index`.
21
+ * Uses the cue's own `EndMs`, else the next cue's `StartMs`, else `+Infinity`.
22
+ */
23
+ export declare function resolveCueEndMs(cues: MediaTranscriptCue[], index: number): number;
24
+ //# sourceMappingURL=cue-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cue-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/media-player/cue-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAe9G;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,kBAAkB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAUjF"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Pure computation of the active transcript cue for a given playback position.
3
+ *
4
+ * A cue is active when `currentMs` falls within `[StartMs, end)` where `end` is:
5
+ * - the cue's own `EndMs` when present, OR
6
+ * - the next cue's `StartMs` (the cue runs until the next one begins), OR
7
+ * - `+Infinity` for the final cue with no `EndMs` (runs to the end of the media).
8
+ *
9
+ * Cues are assumed to be in chronological order by `StartMs`. When more than one
10
+ * cue qualifies (overlapping ranges), the LAST qualifying cue wins, which matches
11
+ * the "most recently started" intuition for transcripts.
12
+ *
13
+ * @param currentMs current playback position in milliseconds
14
+ * @param cues the ordered transcript cues
15
+ * @returns the index of the active cue, or -1 if none is active (e.g. before the first cue)
16
+ */
17
+ export function computeActiveCueIndex(currentMs, cues) {
18
+ if (!cues || cues.length === 0) {
19
+ return -1;
20
+ }
21
+ let activeIndex = -1;
22
+ for (let i = 0; i < cues.length; i++) {
23
+ const cue = cues[i];
24
+ const start = cue.StartMs;
25
+ const end = resolveCueEndMs(cues, i);
26
+ if (currentMs >= start && currentMs < end) {
27
+ activeIndex = i; // last qualifying wins
28
+ }
29
+ }
30
+ return activeIndex;
31
+ }
32
+ /**
33
+ * Resolves the effective end time (exclusive) of the cue at `index`.
34
+ * Uses the cue's own `EndMs`, else the next cue's `StartMs`, else `+Infinity`.
35
+ */
36
+ export function resolveCueEndMs(cues, index) {
37
+ const cue = cues[index];
38
+ if (cue.EndMs != null) {
39
+ return cue.EndMs;
40
+ }
41
+ const next = cues[index + 1];
42
+ if (next) {
43
+ return next.StartMs;
44
+ }
45
+ return Number.POSITIVE_INFINITY;
46
+ }
47
+ //# sourceMappingURL=cue-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cue-utils.js","sourceRoot":"","sources":["../../../src/lib/media-player/cue-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAiB,EAAE,IAA6C;IACpG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,CAAC,CAAC;IACZ,CAAC;IAED,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC;QAC1B,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACrC,IAAI,SAAS,IAAI,KAAK,IAAI,SAAS,GAAG,GAAG,EAAE,CAAC;YAC1C,WAAW,GAAG,CAAC,CAAC,CAAC,uBAAuB;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAA0B,EAAE,KAAa;IACvE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,GAAG,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QACtB,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC7B,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IACD,OAAO,MAAM,CAAC,iBAAiB,CAAC;AAClC,CAAC","sourcesContent":["import { MediaTranscriptCue } from '../media-player.types';\n\n/**\n * Pure computation of the active transcript cue for a given playback position.\n *\n * A cue is active when `currentMs` falls within `[StartMs, end)` where `end` is:\n * - the cue's own `EndMs` when present, OR\n * - the next cue's `StartMs` (the cue runs until the next one begins), OR\n * - `+Infinity` for the final cue with no `EndMs` (runs to the end of the media).\n *\n * Cues are assumed to be in chronological order by `StartMs`. When more than one\n * cue qualifies (overlapping ranges), the LAST qualifying cue wins, which matches\n * the \"most recently started\" intuition for transcripts.\n *\n * @param currentMs current playback position in milliseconds\n * @param cues the ordered transcript cues\n * @returns the index of the active cue, or -1 if none is active (e.g. before the first cue)\n */\nexport function computeActiveCueIndex(currentMs: number, cues: MediaTranscriptCue[] | null | undefined): number {\n if (!cues || cues.length === 0) {\n return -1;\n }\n\n let activeIndex = -1;\n for (let i = 0; i < cues.length; i++) {\n const cue = cues[i];\n const start = cue.StartMs;\n const end = resolveCueEndMs(cues, i);\n if (currentMs >= start && currentMs < end) {\n activeIndex = i; // last qualifying wins\n }\n }\n return activeIndex;\n}\n\n/**\n * Resolves the effective end time (exclusive) of the cue at `index`.\n * Uses the cue's own `EndMs`, else the next cue's `StartMs`, else `+Infinity`.\n */\nexport function resolveCueEndMs(cues: MediaTranscriptCue[], index: number): number {\n const cue = cues[index];\n if (cue.EndMs != null) {\n return cue.EndMs;\n }\n const next = cues[index + 1];\n if (next) {\n return next.StartMs;\n }\n return Number.POSITIVE_INFINITY;\n}\n"]}