@memberjunction/ng-media-player 0.0.1 → 5.45.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.
- package/README.md +241 -28
- package/dist/lib/media-player/cue-utils.d.ts +24 -0
- package/dist/lib/media-player/cue-utils.d.ts.map +1 -0
- package/dist/lib/media-player/cue-utils.js +47 -0
- package/dist/lib/media-player/cue-utils.js.map +1 -0
- package/dist/lib/media-player/media-player.component.d.ts +326 -0
- package/dist/lib/media-player/media-player.component.d.ts.map +1 -0
- package/dist/lib/media-player/media-player.component.js +1545 -0
- package/dist/lib/media-player/media-player.component.js.map +1 -0
- package/dist/lib/media-player/playback-state.d.ts +46 -0
- package/dist/lib/media-player/playback-state.d.ts.map +1 -0
- package/dist/lib/media-player/playback-state.js +86 -0
- package/dist/lib/media-player/playback-state.js.map +1 -0
- package/dist/lib/media-player/transcript-layout.d.ts +35 -0
- package/dist/lib/media-player/transcript-layout.d.ts.map +1 -0
- package/dist/lib/media-player/transcript-layout.js +37 -0
- package/dist/lib/media-player/transcript-layout.js.map +1 -0
- package/dist/lib/media-player/waveform-utils.d.ts +45 -0
- package/dist/lib/media-player/waveform-utils.d.ts.map +1 -0
- package/dist/lib/media-player/waveform-utils.js +108 -0
- package/dist/lib/media-player/waveform-utils.js.map +1 -0
- package/dist/lib/media-player.types.d.ts +107 -0
- package/dist/lib/media-player.types.d.ts.map +1 -0
- package/dist/lib/media-player.types.js +9 -0
- package/dist/lib/media-player.types.js.map +1 -0
- package/dist/lib/storage-media-player/storage-media-player.component.d.ts +77 -0
- package/dist/lib/storage-media-player/storage-media-player.component.d.ts.map +1 -0
- package/dist/lib/storage-media-player/storage-media-player.component.js +283 -0
- package/dist/lib/storage-media-player/storage-media-player.component.js.map +1 -0
- package/dist/public-api.d.ts +8 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +11 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +36 -7
package/README.md
CHANGED
|
@@ -1,45 +1,258 @@
|
|
|
1
1
|
# @memberjunction/ng-media-player
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @memberjunction/ng-media-player
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Overview
|
|
10
12
|
|
|
11
|
-
This package
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
style Wrapper fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
36
|
+
style Generic fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
37
|
+
```
|
|
23
38
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
### Generic player (plain URLs)
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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"]}
|