@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.
- 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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, framework-agnostic waveform math for the media player.
|
|
3
|
+
*
|
|
4
|
+
* These helpers turn a raw mono PCM channel (a `Float32Array` of sample amplitudes,
|
|
5
|
+
* typically in the `[-1, 1]` range that the Web Audio API produces) into a small,
|
|
6
|
+
* fixed-size array of normalized `0..1` peak values — one per rendered waveform bar.
|
|
7
|
+
*
|
|
8
|
+
* They carry no DOM / Angular / `AudioContext` dependency so they can be unit-tested
|
|
9
|
+
* in isolation, mirroring how `cue-utils.ts` splits the cue math out of the component.
|
|
10
|
+
*/
|
|
11
|
+
/** The default number of bars a waveform renders at. */
|
|
12
|
+
export const DEFAULT_WAVEFORM_BARS = 160;
|
|
13
|
+
/**
|
|
14
|
+
* Downsamples a raw mono sample buffer into `barCount` per-bucket amplitudes and
|
|
15
|
+
* normalizes the result to `0..1` (the loudest bucket becomes `1`).
|
|
16
|
+
*
|
|
17
|
+
* The samples are partitioned into `barCount` contiguous, (near-)equal buckets in
|
|
18
|
+
* index order; each bucket is reduced to one amplitude via `aggregation`:
|
|
19
|
+
* - `'max-abs'` (default) — the maximum absolute sample in the bucket (crisp peaks),
|
|
20
|
+
* - `'rms'` — the root-mean-square of the bucket (smoother, energy-weighted).
|
|
21
|
+
*
|
|
22
|
+
* Edge cases:
|
|
23
|
+
* - empty / null input → an array of `barCount` zeros,
|
|
24
|
+
* - fewer samples than bars → every sample still lands in a bucket; trailing empty
|
|
25
|
+
* buckets are `0` (no out-of-bounds reads),
|
|
26
|
+
* - a silent or constant-zero buffer → all zeros (normalization is skipped when the
|
|
27
|
+
* peak is `0`, so we never divide by zero).
|
|
28
|
+
*
|
|
29
|
+
* @param samples raw mono channel data (e.g. `AudioBuffer.getChannelData(0)`)
|
|
30
|
+
* @param barCount number of output bars (clamped to `>= 1`)
|
|
31
|
+
* @param aggregation per-bucket reducer (default `'max-abs'`)
|
|
32
|
+
* @returns a `barCount`-length array of normalized `0..1` peaks
|
|
33
|
+
*/
|
|
34
|
+
export function downsamplePeaks(samples, barCount = DEFAULT_WAVEFORM_BARS, aggregation = 'max-abs') {
|
|
35
|
+
const bars = Math.max(1, Math.floor(barCount));
|
|
36
|
+
const length = samples ? samples.length : 0;
|
|
37
|
+
if (length === 0) {
|
|
38
|
+
return new Array(bars).fill(0);
|
|
39
|
+
}
|
|
40
|
+
const raw = aggregateBuckets(samples, length, bars, aggregation);
|
|
41
|
+
return normalizePeaks(raw);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reduces a sample buffer to `bars` per-bucket amplitudes (un-normalized).
|
|
45
|
+
* Buckets are contiguous and index-ordered; the last bucket absorbs any remainder.
|
|
46
|
+
*/
|
|
47
|
+
function aggregateBuckets(samples, length, bars, aggregation) {
|
|
48
|
+
const out = new Array(bars).fill(0);
|
|
49
|
+
const bucketSize = length / bars;
|
|
50
|
+
for (let b = 0; b < bars; b++) {
|
|
51
|
+
const start = Math.floor(b * bucketSize);
|
|
52
|
+
const end = b === bars - 1 ? length : Math.floor((b + 1) * bucketSize);
|
|
53
|
+
out[b] = reduceBucket(samples, start, end, aggregation);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** Reduces one bucket `[start, end)` of `samples` to a single amplitude. */
|
|
58
|
+
function reduceBucket(samples, start, end, aggregation) {
|
|
59
|
+
if (end <= start) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
if (aggregation === 'rms') {
|
|
63
|
+
let sumSquares = 0;
|
|
64
|
+
for (let i = start; i < end; i++) {
|
|
65
|
+
const v = samples[i];
|
|
66
|
+
sumSquares += v * v;
|
|
67
|
+
}
|
|
68
|
+
return Math.sqrt(sumSquares / (end - start));
|
|
69
|
+
}
|
|
70
|
+
// 'max-abs'
|
|
71
|
+
let peak = 0;
|
|
72
|
+
for (let i = start; i < end; i++) {
|
|
73
|
+
const v = Math.abs(samples[i]);
|
|
74
|
+
if (v > peak) {
|
|
75
|
+
peak = v;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return peak;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Normalizes an array of non-negative amplitudes to `0..1` by dividing by the maximum.
|
|
82
|
+
* Returns a copy. When the maximum is `0` (silence) or non-finite, returns the values
|
|
83
|
+
* clamped to `[0, 1]` without scaling (so an all-zero input stays all-zero).
|
|
84
|
+
*/
|
|
85
|
+
export function normalizePeaks(values) {
|
|
86
|
+
let max = 0;
|
|
87
|
+
for (const v of values) {
|
|
88
|
+
const a = Math.abs(v);
|
|
89
|
+
if (a > max) {
|
|
90
|
+
max = a;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (max <= 0 || !isFinite(max)) {
|
|
94
|
+
return values.map((v) => clamp01(v));
|
|
95
|
+
}
|
|
96
|
+
return values.map((v) => clamp01(Math.abs(v) / max));
|
|
97
|
+
}
|
|
98
|
+
/** Clamps a number to the `[0, 1]` range. */
|
|
99
|
+
export function clamp01(v) {
|
|
100
|
+
if (!isFinite(v) || v < 0) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
if (v > 1) {
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
return v;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=waveform-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waveform-utils.js","sourceRoot":"","sources":["../../../src/lib/media-player/waveform-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure, framework-agnostic waveform math for the media player.\n *\n * These helpers turn a raw mono PCM channel (a `Float32Array` of sample amplitudes,\n * typically in the `[-1, 1]` range that the Web Audio API produces) into a small,\n * fixed-size array of normalized `0..1` peak values — one per rendered waveform bar.\n *\n * They carry no DOM / Angular / `AudioContext` dependency so they can be unit-tested\n * in isolation, mirroring how `cue-utils.ts` splits the cue math out of the component.\n */\n\n/** How each downsample bucket is reduced to a single amplitude. */\nexport type WaveformAggregation = 'max-abs' | 'rms';\n\n/** The default number of bars a waveform renders at. */\nexport const DEFAULT_WAVEFORM_BARS = 160;\n\n/**\n * Downsamples a raw mono sample buffer into `barCount` per-bucket amplitudes and\n * normalizes the result to `0..1` (the loudest bucket becomes `1`).\n *\n * The samples are partitioned into `barCount` contiguous, (near-)equal buckets in\n * index order; each bucket is reduced to one amplitude via `aggregation`:\n * - `'max-abs'` (default) — the maximum absolute sample in the bucket (crisp peaks),\n * - `'rms'` — the root-mean-square of the bucket (smoother, energy-weighted).\n *\n * Edge cases:\n * - empty / null input → an array of `barCount` zeros,\n * - fewer samples than bars → every sample still lands in a bucket; trailing empty\n * buckets are `0` (no out-of-bounds reads),\n * - a silent or constant-zero buffer → all zeros (normalization is skipped when the\n * peak is `0`, so we never divide by zero).\n *\n * @param samples raw mono channel data (e.g. `AudioBuffer.getChannelData(0)`)\n * @param barCount number of output bars (clamped to `>= 1`)\n * @param aggregation per-bucket reducer (default `'max-abs'`)\n * @returns a `barCount`-length array of normalized `0..1` peaks\n */\nexport function downsamplePeaks(\n samples: Float32Array | number[] | null | undefined,\n barCount: number = DEFAULT_WAVEFORM_BARS,\n aggregation: WaveformAggregation = 'max-abs',\n): number[] {\n const bars = Math.max(1, Math.floor(barCount));\n const length = samples ? samples.length : 0;\n if (length === 0) {\n return new Array<number>(bars).fill(0);\n }\n\n const raw = aggregateBuckets(samples as Float32Array | number[], length, bars, aggregation);\n return normalizePeaks(raw);\n}\n\n/**\n * Reduces a sample buffer to `bars` per-bucket amplitudes (un-normalized).\n * Buckets are contiguous and index-ordered; the last bucket absorbs any remainder.\n */\nfunction aggregateBuckets(\n samples: Float32Array | number[],\n length: number,\n bars: number,\n aggregation: WaveformAggregation,\n): number[] {\n const out = new Array<number>(bars).fill(0);\n const bucketSize = length / bars;\n\n for (let b = 0; b < bars; b++) {\n const start = Math.floor(b * bucketSize);\n const end = b === bars - 1 ? length : Math.floor((b + 1) * bucketSize);\n out[b] = reduceBucket(samples, start, end, aggregation);\n }\n return out;\n}\n\n/** Reduces one bucket `[start, end)` of `samples` to a single amplitude. */\nfunction reduceBucket(\n samples: Float32Array | number[],\n start: number,\n end: number,\n aggregation: WaveformAggregation,\n): number {\n if (end <= start) {\n return 0;\n }\n if (aggregation === 'rms') {\n let sumSquares = 0;\n for (let i = start; i < end; i++) {\n const v = samples[i];\n sumSquares += v * v;\n }\n return Math.sqrt(sumSquares / (end - start));\n }\n // 'max-abs'\n let peak = 0;\n for (let i = start; i < end; i++) {\n const v = Math.abs(samples[i]);\n if (v > peak) {\n peak = v;\n }\n }\n return peak;\n}\n\n/**\n * Normalizes an array of non-negative amplitudes to `0..1` by dividing by the maximum.\n * Returns a copy. When the maximum is `0` (silence) or non-finite, returns the values\n * clamped to `[0, 1]` without scaling (so an all-zero input stays all-zero).\n */\nexport function normalizePeaks(values: number[]): number[] {\n let max = 0;\n for (const v of values) {\n const a = Math.abs(v);\n if (a > max) {\n max = a;\n }\n }\n if (max <= 0 || !isFinite(max)) {\n return values.map((v) => clamp01(v));\n }\n return values.map((v) => clamp01(Math.abs(v) / max));\n}\n\n/** Clamps a number to the `[0, 1]` range. */\nexport function clamp01(v: number): number {\n if (!isFinite(v) || v < 0) {\n return 0;\n }\n if (v > 1) {\n return 1;\n }\n return v;\n}\n"]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type contracts for the MJ media player components.
|
|
3
|
+
*
|
|
4
|
+
* These types are framework-agnostic and carry no MemberJunction-core dependency —
|
|
5
|
+
* they describe the data the generic {@link MJMediaPlayerComponent} renders and the
|
|
6
|
+
* shape of its cancelable / informational events.
|
|
7
|
+
*/
|
|
8
|
+
/** Whether a track is rendered with an `<audio>` or `<video>` element. */
|
|
9
|
+
export type MediaKind = 'audio' | 'video';
|
|
10
|
+
/**
|
|
11
|
+
* The high-level playback lifecycle of the PRIMARY media element, derived from its
|
|
12
|
+
* native HTMLMediaElement events.
|
|
13
|
+
*
|
|
14
|
+
* - `idle` — no source / nothing happening yet.
|
|
15
|
+
* - `loading` — initial fetch of the source (post-`loadstart`, pre-`canplay`); the stage
|
|
16
|
+
* shows a "Loading…" indicator.
|
|
17
|
+
* - `buffering` — ran out of buffered data mid-playback, or seeking into un-buffered
|
|
18
|
+
* territory (e.g. a ±30s skip or a scrub); the stage shows "Buffering…".
|
|
19
|
+
* - `ready` — enough data buffered to play, but not currently playing (post-`canplay`,
|
|
20
|
+
* paused at position).
|
|
21
|
+
* - `playing` — actively playing.
|
|
22
|
+
* - `paused` — user-paused (distinct from `ready`, which is "ready but never started /
|
|
23
|
+
* stopped at end-of-buffer").
|
|
24
|
+
* - `ended` — playback reached the end of the media.
|
|
25
|
+
* - `error` — the element raised an `error` (unplayable source, decode failure, etc.).
|
|
26
|
+
*/
|
|
27
|
+
export type MediaPlaybackState = 'idle' | 'loading' | 'buffering' | 'ready' | 'playing' | 'paused' | 'ended' | 'error';
|
|
28
|
+
/**
|
|
29
|
+
* A single playable media source. A player may receive one or many.
|
|
30
|
+
* For multiple video tracks the player renders a responsive grid; for a single
|
|
31
|
+
* audio track it renders the transport bar (optionally with a waveform).
|
|
32
|
+
*/
|
|
33
|
+
export interface MediaTrack {
|
|
34
|
+
/** Stable identifier — used for `track`-by in the template and event correlation. */
|
|
35
|
+
Id: string;
|
|
36
|
+
/** Audio vs. video — drives which element the player creates. */
|
|
37
|
+
Kind: MediaKind;
|
|
38
|
+
/** The media URL (http(s) or a blob: object URL). */
|
|
39
|
+
Url: string;
|
|
40
|
+
/** Optional MIME type hint (e.g. `audio/mpeg`, `video/mp4`). */
|
|
41
|
+
MimeType?: string;
|
|
42
|
+
/** Optional human label (e.g. a speaker / camera name) shown in multi-track layouts. */
|
|
43
|
+
Label?: string;
|
|
44
|
+
/** Optional poster image for video tracks shown before playback. */
|
|
45
|
+
PosterUrl?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional precomputed waveform peaks — an array of normalized `0..1` amplitude
|
|
48
|
+
* values, one per rendered bar (left → right across the track's duration).
|
|
49
|
+
*
|
|
50
|
+
* This is the **forward path** for server-precomputed peaks + streaming media:
|
|
51
|
+
* when a track supplies `Peaks`, the player renders them directly and performs
|
|
52
|
+
* **no client-side decoding** (no fetch, no `AudioContext`). When `Peaks` is
|
|
53
|
+
* absent (and the waveform is enabled for an audio track), the player extracts
|
|
54
|
+
* real peaks client-side by decoding {@link Url}; on any decode failure it falls
|
|
55
|
+
* back to a plain progress bar.
|
|
56
|
+
*
|
|
57
|
+
* Values are clamped to `[0, 1]` at render time; provide as many entries as you
|
|
58
|
+
* like — the player renders one bar per value.
|
|
59
|
+
*/
|
|
60
|
+
Peaks?: number[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A single transcript cue (a span of spoken text tied to a time range).
|
|
64
|
+
* `EndMs` is optional — when absent, the active-cue window runs until the next cue's `StartMs`.
|
|
65
|
+
*/
|
|
66
|
+
export interface MediaTranscriptCue {
|
|
67
|
+
/** Stable identifier for the cue. */
|
|
68
|
+
Id: string;
|
|
69
|
+
/** Cue start time, in milliseconds from the start of the media. */
|
|
70
|
+
StartMs: number;
|
|
71
|
+
/** Optional cue end time, in milliseconds. When omitted, the next cue's start is used. */
|
|
72
|
+
EndMs?: number;
|
|
73
|
+
/** Optional speaker label — color-coded in the transcript panel. */
|
|
74
|
+
SpeakerLabel?: string;
|
|
75
|
+
/** The spoken text for this cue. */
|
|
76
|
+
Text: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Base shape for all cancelable "Before*" events. Handlers may set `Cancel = true`
|
|
80
|
+
* to abort the pending action (mirrors the ng-conversations `beforeAgentTurn` pattern).
|
|
81
|
+
*/
|
|
82
|
+
export interface MediaPlayerCancelableEvent {
|
|
83
|
+
/** Set to `true` in a handler to prevent the pending action. */
|
|
84
|
+
Cancel: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Cancelable seek event. `FromMs`/`ToMs` describe the pending reposition. */
|
|
87
|
+
export interface MediaSeekEvent extends MediaPlayerCancelableEvent {
|
|
88
|
+
/** Current playback position, in milliseconds, before the seek. */
|
|
89
|
+
FromMs: number;
|
|
90
|
+
/** Target playback position, in milliseconds, after the seek. */
|
|
91
|
+
ToMs: number;
|
|
92
|
+
}
|
|
93
|
+
/** Cancelable playback-rate change event. */
|
|
94
|
+
export interface MediaRateEvent extends MediaPlayerCancelableEvent {
|
|
95
|
+
/** Current playback rate before the change. */
|
|
96
|
+
FromRate: number;
|
|
97
|
+
/** Target playback rate after the change. */
|
|
98
|
+
ToRate: number;
|
|
99
|
+
}
|
|
100
|
+
/** Informational event carrying a transcript cue and its index. */
|
|
101
|
+
export interface MediaCueEvent {
|
|
102
|
+
/** The cue involved in the event. */
|
|
103
|
+
Cue: MediaTranscriptCue;
|
|
104
|
+
/** The cue's index within the `Transcript` array. */
|
|
105
|
+
Index: number;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=media-player.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-player.types.d.ts","sourceRoot":"","sources":["../../src/lib/media-player.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,SAAS,GACT,WAAW,GACX,OAAO,GACP,SAAS,GACT,QAAQ,GACR,OAAO,GACP,OAAO,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,qFAAqF;IACrF,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,IAAI,EAAE,SAAS,CAAC;IAChB,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,gEAAgE;IAChE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,cAAe,SAAQ,0BAA0B;IAChE,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAe,SAAQ,0BAA0B;IAChE,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,GAAG,EAAE,kBAAkB,CAAC;IACxB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;CACf"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type contracts for the MJ media player components.
|
|
3
|
+
*
|
|
4
|
+
* These types are framework-agnostic and carry no MemberJunction-core dependency —
|
|
5
|
+
* they describe the data the generic {@link MJMediaPlayerComponent} renders and the
|
|
6
|
+
* shape of its cancelable / informational events.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=media-player.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-player.types.js","sourceRoot":"","sources":["../../src/lib/media-player.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Public type contracts for the MJ media player components.\n *\n * These types are framework-agnostic and carry no MemberJunction-core dependency —\n * they describe the data the generic {@link MJMediaPlayerComponent} renders and the\n * shape of its cancelable / informational events.\n */\n\n/** Whether a track is rendered with an `<audio>` or `<video>` element. */\nexport type MediaKind = 'audio' | 'video';\n\n/**\n * The high-level playback lifecycle of the PRIMARY media element, derived from its\n * native HTMLMediaElement events.\n *\n * - `idle` — no source / nothing happening yet.\n * - `loading` — initial fetch of the source (post-`loadstart`, pre-`canplay`); the stage\n * shows a \"Loading…\" indicator.\n * - `buffering` — ran out of buffered data mid-playback, or seeking into un-buffered\n * territory (e.g. a ±30s skip or a scrub); the stage shows \"Buffering…\".\n * - `ready` — enough data buffered to play, but not currently playing (post-`canplay`,\n * paused at position).\n * - `playing` — actively playing.\n * - `paused` — user-paused (distinct from `ready`, which is \"ready but never started /\n * stopped at end-of-buffer\").\n * - `ended` — playback reached the end of the media.\n * - `error` — the element raised an `error` (unplayable source, decode failure, etc.).\n */\nexport type MediaPlaybackState =\n | 'idle'\n | 'loading'\n | 'buffering'\n | 'ready'\n | 'playing'\n | 'paused'\n | 'ended'\n | 'error';\n\n/**\n * A single playable media source. A player may receive one or many.\n * For multiple video tracks the player renders a responsive grid; for a single\n * audio track it renders the transport bar (optionally with a waveform).\n */\nexport interface MediaTrack {\n /** Stable identifier — used for `track`-by in the template and event correlation. */\n Id: string;\n /** Audio vs. video — drives which element the player creates. */\n Kind: MediaKind;\n /** The media URL (http(s) or a blob: object URL). */\n Url: string;\n /** Optional MIME type hint (e.g. `audio/mpeg`, `video/mp4`). */\n MimeType?: string;\n /** Optional human label (e.g. a speaker / camera name) shown in multi-track layouts. */\n Label?: string;\n /** Optional poster image for video tracks shown before playback. */\n PosterUrl?: string;\n /**\n * Optional precomputed waveform peaks — an array of normalized `0..1` amplitude\n * values, one per rendered bar (left → right across the track's duration).\n *\n * This is the **forward path** for server-precomputed peaks + streaming media:\n * when a track supplies `Peaks`, the player renders them directly and performs\n * **no client-side decoding** (no fetch, no `AudioContext`). When `Peaks` is\n * absent (and the waveform is enabled for an audio track), the player extracts\n * real peaks client-side by decoding {@link Url}; on any decode failure it falls\n * back to a plain progress bar.\n *\n * Values are clamped to `[0, 1]` at render time; provide as many entries as you\n * like — the player renders one bar per value.\n */\n Peaks?: number[];\n}\n\n/**\n * A single transcript cue (a span of spoken text tied to a time range).\n * `EndMs` is optional — when absent, the active-cue window runs until the next cue's `StartMs`.\n */\nexport interface MediaTranscriptCue {\n /** Stable identifier for the cue. */\n Id: string;\n /** Cue start time, in milliseconds from the start of the media. */\n StartMs: number;\n /** Optional cue end time, in milliseconds. When omitted, the next cue's start is used. */\n EndMs?: number;\n /** Optional speaker label — color-coded in the transcript panel. */\n SpeakerLabel?: string;\n /** The spoken text for this cue. */\n Text: string;\n}\n\n/**\n * Base shape for all cancelable \"Before*\" events. Handlers may set `Cancel = true`\n * to abort the pending action (mirrors the ng-conversations `beforeAgentTurn` pattern).\n */\nexport interface MediaPlayerCancelableEvent {\n /** Set to `true` in a handler to prevent the pending action. */\n Cancel: boolean;\n}\n\n/** Cancelable seek event. `FromMs`/`ToMs` describe the pending reposition. */\nexport interface MediaSeekEvent extends MediaPlayerCancelableEvent {\n /** Current playback position, in milliseconds, before the seek. */\n FromMs: number;\n /** Target playback position, in milliseconds, after the seek. */\n ToMs: number;\n}\n\n/** Cancelable playback-rate change event. */\nexport interface MediaRateEvent extends MediaPlayerCancelableEvent {\n /** Current playback rate before the change. */\n FromRate: number;\n /** Target playback rate after the change. */\n ToRate: number;\n}\n\n/** Informational event carrying a transcript cue and its index. */\nexport interface MediaCueEvent {\n /** The cue involved in the event. */\n Cue: MediaTranscriptCue;\n /** The cue's index within the `Transcript` array. */\n Index: number;\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { OnInit } from '@angular/core';
|
|
2
|
+
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
3
|
+
import { MediaTrack, MediaTranscriptCue } from '../media-player.types';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
/**
|
|
6
|
+
* `mj-storage-media-player` — an MJStorage-bound wrapper around {@link MJMediaPlayerComponent}.
|
|
7
|
+
*
|
|
8
|
+
* Resolves one or more MJ Storage file IDs into playable {@link MediaTrack}s by minting a
|
|
9
|
+
* short-lived authenticated streaming URL per file via the `CreateMediaAccessToken` GraphQL
|
|
10
|
+
* mutation, and handing those URLs to the generic player. The `<audio>`/`<video>` element
|
|
11
|
+
* streams each URL natively over HTTP Range (progressive playback + seek-before-download for
|
|
12
|
+
* large video) instead of base64'ing the whole file over GraphQL. Surfaces graceful loading /
|
|
13
|
+
* no-access / empty states.
|
|
14
|
+
*
|
|
15
|
+
* Waveform peaks: when the recording has a `peaks.json` sidecar in storage, `CreateMediaAccessToken`
|
|
16
|
+
* returns precomputed peaks and this wrapper sets them on the {@link MediaTrack.Peaks}, so the player
|
|
17
|
+
* renders the real waveform instantly with NO client-side fetch/decode. When no sidecar exists, audio
|
|
18
|
+
* waveforms still decode client-side via the streaming URL (the waveform's one-time `fetch(Url)`
|
|
19
|
+
* issues a no-Range GET, which returns the full file — fine for audio). Video shows the progress bar.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MJStorageMediaPlayerComponent extends BaseAngularComponent implements OnInit {
|
|
22
|
+
private cdr;
|
|
23
|
+
/** Convenience single-file id. Combined with {@link FileIDs}; duplicates are de-duped. */
|
|
24
|
+
set FileID(value: string | null);
|
|
25
|
+
get FileID(): string | null;
|
|
26
|
+
private _fileID;
|
|
27
|
+
/** Multi-track file ids (e.g. separate meeting streams). */
|
|
28
|
+
set FileIDs(value: string[]);
|
|
29
|
+
get FileIDs(): string[];
|
|
30
|
+
private _fileIDs;
|
|
31
|
+
/** Transcript cues forwarded to the generic player. */
|
|
32
|
+
Transcript: MediaTranscriptCue[] | null;
|
|
33
|
+
ShowTranscript: boolean;
|
|
34
|
+
/** Whether the transcript show/hide toggle button renders. Forwarded to the generic player. */
|
|
35
|
+
ShowTranscriptToggle: boolean;
|
|
36
|
+
ShowSpeedControl: boolean;
|
|
37
|
+
ShowSkipControls: boolean;
|
|
38
|
+
ShowVolume: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Forwarded to the generic player. On by default — the streaming URL is same-origin (served by
|
|
41
|
+
* MJAPI) and a no-Range GET returns the full file, so client-side peak extraction decodes cleanly.
|
|
42
|
+
*/
|
|
43
|
+
ShowWaveform: boolean;
|
|
44
|
+
ShowFullscreen: boolean;
|
|
45
|
+
SkipSeconds: number;
|
|
46
|
+
PlaybackRates: number[];
|
|
47
|
+
Autoplay: boolean;
|
|
48
|
+
StartAtMs: number | null;
|
|
49
|
+
/** Where the transcript sits. Defaults to `'bottom'` (matches the generic player). */
|
|
50
|
+
TranscriptPosition: 'side' | 'bottom';
|
|
51
|
+
private _initialized;
|
|
52
|
+
private _resolvedTracks;
|
|
53
|
+
private _isLoading;
|
|
54
|
+
private _accessError;
|
|
55
|
+
private _resolveToken;
|
|
56
|
+
get ResolvedTracks(): MediaTrack[];
|
|
57
|
+
get IsLoading(): boolean;
|
|
58
|
+
get AccessError(): string | null;
|
|
59
|
+
get HasFileIds(): boolean;
|
|
60
|
+
ngOnInit(): Promise<void>;
|
|
61
|
+
/** Resolves all configured file ids into playable streaming tracks. Re-entrant-safe via a token. */
|
|
62
|
+
private resolveTracks;
|
|
63
|
+
/** Mints an authenticated streaming-URL token for one file id via the active provider. */
|
|
64
|
+
private mintAccessToken;
|
|
65
|
+
/**
|
|
66
|
+
* Builds a MediaTrack from the streaming URL. Kind is MIME-derived (defaults to audio). When the
|
|
67
|
+
* server supplied precomputed waveform `peaks` (from a peaks.json sidecar), they're attached so the
|
|
68
|
+
* player renders the real waveform instantly with NO extra fetch/decode; otherwise the player
|
|
69
|
+
* extracts peaks client-side from the streaming URL.
|
|
70
|
+
*/
|
|
71
|
+
private buildTrack;
|
|
72
|
+
private collectFileIds;
|
|
73
|
+
private sameIds;
|
|
74
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<MJStorageMediaPlayerComponent, never>;
|
|
75
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<MJStorageMediaPlayerComponent, "mj-storage-media-player", never, { "FileID": { "alias": "FileID"; "required": false; }; "FileIDs": { "alias": "FileIDs"; "required": false; }; "Transcript": { "alias": "Transcript"; "required": false; }; "ShowTranscript": { "alias": "ShowTranscript"; "required": false; }; "ShowTranscriptToggle": { "alias": "ShowTranscriptToggle"; "required": false; }; "ShowSpeedControl": { "alias": "ShowSpeedControl"; "required": false; }; "ShowSkipControls": { "alias": "ShowSkipControls"; "required": false; }; "ShowVolume": { "alias": "ShowVolume"; "required": false; }; "ShowWaveform": { "alias": "ShowWaveform"; "required": false; }; "ShowFullscreen": { "alias": "ShowFullscreen"; "required": false; }; "SkipSeconds": { "alias": "SkipSeconds"; "required": false; }; "PlaybackRates": { "alias": "PlaybackRates"; "required": false; }; "Autoplay": { "alias": "Autoplay"; "required": false; }; "StartAtMs": { "alias": "StartAtMs"; "required": false; }; "TranscriptPosition": { "alias": "TranscriptPosition"; "required": false; }; }, {}, never, never, true, never>;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=storage-media-player.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-media-player.component.d.ts","sourceRoot":"","sources":["../../../src/lib/storage-media-player/storage-media-player.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,MAAM,EAEP,MAAM,eAAe,CAAC;AAIvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;;AAkCvE;;;;;;;;;;;;;;;GAeG;AACH,qBAQa,6BAA8B,SAAQ,oBAAqB,YAAW,MAAM;IACvF,OAAO,CAAC,GAAG,CAA6B;IAMxC,0FAA0F;IAC1F,IACI,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAO9B;IACD,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,CAE1B;IACD,OAAO,CAAC,OAAO,CAAuB;IAEtC,4DAA4D;IAC5D,IACI,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAQ1B;IACD,IAAI,OAAO,IAAI,MAAM,EAAE,CAEtB;IACD,OAAO,CAAC,QAAQ,CAAgB;IAEhC,uDAAuD;IAC9C,UAAU,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAQ;IAM/C,cAAc,UAAQ;IAC/B,+FAA+F;IACtF,oBAAoB,UAAQ;IAC5B,gBAAgB,UAAQ;IACxB,gBAAgB,UAAQ;IACxB,UAAU,UAAQ;IAC3B;;;OAGG;IACM,YAAY,UAAQ;IACpB,cAAc,UAAQ;IACtB,WAAW,SAAM;IACjB,aAAa,EAAE,MAAM,EAAE,CAA0B;IACjD,QAAQ,UAAS;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IACzC,sFAAsF;IAC7E,kBAAkB,EAAE,MAAM,GAAG,QAAQ,CAAY;IAM1D,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAAoB;IAC3C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAK;IAE1B,IAAI,cAAc,IAAI,UAAU,EAAE,CAEjC;IACD,IAAI,SAAS,IAAI,OAAO,CAEvB;IACD,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IACD,IAAI,UAAU,IAAI,OAAO,CAExB;IAEK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAS/B,oGAAoG;YACtF,aAAa;IA2C3B,0FAA0F;YAC5E,eAAe;IAY7B;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,OAAO;yCA1LJ,6BAA6B;2CAA7B,6BAA6B;CAgMzC"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, inject, } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { LogError } from '@memberjunction/core';
|
|
4
|
+
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
5
|
+
import { MJMediaPlayerComponent } from '../media-player/media-player.component';
|
|
6
|
+
import * as i0 from "@angular/core";
|
|
7
|
+
function MJStorageMediaPlayerComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
8
|
+
i0.ɵɵelementStart(0, "div", 0);
|
|
9
|
+
i0.ɵɵelement(1, "i", 4);
|
|
10
|
+
i0.ɵɵelementStart(2, "span");
|
|
11
|
+
i0.ɵɵtext(3, "Loading recording\u2026");
|
|
12
|
+
i0.ɵɵelementEnd()();
|
|
13
|
+
} }
|
|
14
|
+
function MJStorageMediaPlayerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
15
|
+
i0.ɵɵelementStart(0, "div", 1);
|
|
16
|
+
i0.ɵɵelement(1, "i", 5);
|
|
17
|
+
i0.ɵɵelementStart(2, "div", 6)(3, "span", 7);
|
|
18
|
+
i0.ɵɵtext(4, "This recording isn't available \u2014 you may not have access.");
|
|
19
|
+
i0.ɵɵelementEnd();
|
|
20
|
+
i0.ɵɵelementStart(5, "span", 8);
|
|
21
|
+
i0.ɵɵtext(6);
|
|
22
|
+
i0.ɵɵelementEnd()()();
|
|
23
|
+
} if (rf & 2) {
|
|
24
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
25
|
+
i0.ɵɵadvance(6);
|
|
26
|
+
i0.ɵɵtextInterpolate(ctx_r0.AccessError);
|
|
27
|
+
} }
|
|
28
|
+
function MJStorageMediaPlayerComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
29
|
+
i0.ɵɵelementStart(0, "div", 2);
|
|
30
|
+
i0.ɵɵelement(1, "i", 9);
|
|
31
|
+
i0.ɵɵelementStart(2, "span");
|
|
32
|
+
i0.ɵɵtext(3, "No recording to play.");
|
|
33
|
+
i0.ɵɵelementEnd()();
|
|
34
|
+
} }
|
|
35
|
+
function MJStorageMediaPlayerComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
36
|
+
i0.ɵɵelement(0, "mj-media-player", 3);
|
|
37
|
+
} if (rf & 2) {
|
|
38
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
39
|
+
i0.ɵɵproperty("Tracks", ctx_r0.ResolvedTracks)("Transcript", ctx_r0.Transcript)("ShowTranscript", ctx_r0.ShowTranscript)("ShowTranscriptToggle", ctx_r0.ShowTranscriptToggle)("ShowSpeedControl", ctx_r0.ShowSpeedControl)("ShowSkipControls", ctx_r0.ShowSkipControls)("ShowVolume", ctx_r0.ShowVolume)("ShowWaveform", ctx_r0.ShowWaveform)("ShowFullscreen", ctx_r0.ShowFullscreen)("SkipSeconds", ctx_r0.SkipSeconds)("PlaybackRates", ctx_r0.PlaybackRates)("Autoplay", ctx_r0.Autoplay)("StartAtMs", ctx_r0.StartAtMs)("TranscriptPosition", ctx_r0.TranscriptPosition);
|
|
40
|
+
} }
|
|
41
|
+
/**
|
|
42
|
+
* The exact GraphQL mutation this wrapper consumes. Mints a short-lived signed token (after a
|
|
43
|
+
* server-side per-user permission check) and returns the authenticated `/media/<fileId>?token=`
|
|
44
|
+
* streaming URL — which `<audio>`/`<video>` stream natively with HTTP Range (seek-before-download).
|
|
45
|
+
*/
|
|
46
|
+
const CREATE_MEDIA_ACCESS_TOKEN_MUTATION = `mutation CreateMediaAccessToken($fileId: String!) {
|
|
47
|
+
CreateMediaAccessToken(fileId: $fileId) {
|
|
48
|
+
Success
|
|
49
|
+
Url
|
|
50
|
+
ExpiresAt
|
|
51
|
+
MimeType
|
|
52
|
+
Peaks
|
|
53
|
+
ErrorMessage
|
|
54
|
+
}
|
|
55
|
+
}`;
|
|
56
|
+
/**
|
|
57
|
+
* `mj-storage-media-player` — an MJStorage-bound wrapper around {@link MJMediaPlayerComponent}.
|
|
58
|
+
*
|
|
59
|
+
* Resolves one or more MJ Storage file IDs into playable {@link MediaTrack}s by minting a
|
|
60
|
+
* short-lived authenticated streaming URL per file via the `CreateMediaAccessToken` GraphQL
|
|
61
|
+
* mutation, and handing those URLs to the generic player. The `<audio>`/`<video>` element
|
|
62
|
+
* streams each URL natively over HTTP Range (progressive playback + seek-before-download for
|
|
63
|
+
* large video) instead of base64'ing the whole file over GraphQL. Surfaces graceful loading /
|
|
64
|
+
* no-access / empty states.
|
|
65
|
+
*
|
|
66
|
+
* Waveform peaks: when the recording has a `peaks.json` sidecar in storage, `CreateMediaAccessToken`
|
|
67
|
+
* returns precomputed peaks and this wrapper sets them on the {@link MediaTrack.Peaks}, so the player
|
|
68
|
+
* renders the real waveform instantly with NO client-side fetch/decode. When no sidecar exists, audio
|
|
69
|
+
* waveforms still decode client-side via the streaming URL (the waveform's one-time `fetch(Url)`
|
|
70
|
+
* issues a no-Range GET, which returns the full file — fine for audio). Video shows the progress bar.
|
|
71
|
+
*/
|
|
72
|
+
export class MJStorageMediaPlayerComponent extends BaseAngularComponent {
|
|
73
|
+
cdr = inject(ChangeDetectorRef);
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Inputs — file resolution
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/** Convenience single-file id. Combined with {@link FileIDs}; duplicates are de-duped. */
|
|
78
|
+
set FileID(value) {
|
|
79
|
+
if (value !== this._fileID) {
|
|
80
|
+
this._fileID = value;
|
|
81
|
+
if (this._initialized) {
|
|
82
|
+
void this.resolveTracks();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
get FileID() {
|
|
87
|
+
return this._fileID;
|
|
88
|
+
}
|
|
89
|
+
_fileID = null;
|
|
90
|
+
/** Multi-track file ids (e.g. separate meeting streams). */
|
|
91
|
+
set FileIDs(value) {
|
|
92
|
+
const next = value ?? [];
|
|
93
|
+
if (!this.sameIds(next, this._fileIDs)) {
|
|
94
|
+
this._fileIDs = next;
|
|
95
|
+
if (this._initialized) {
|
|
96
|
+
void this.resolveTracks();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
get FileIDs() {
|
|
101
|
+
return this._fileIDs;
|
|
102
|
+
}
|
|
103
|
+
_fileIDs = [];
|
|
104
|
+
/** Transcript cues forwarded to the generic player. */
|
|
105
|
+
Transcript = null;
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Inputs — pass-through feature flags (forwarded to the generic player)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
ShowTranscript = true;
|
|
110
|
+
/** Whether the transcript show/hide toggle button renders. Forwarded to the generic player. */
|
|
111
|
+
ShowTranscriptToggle = true;
|
|
112
|
+
ShowSpeedControl = true;
|
|
113
|
+
ShowSkipControls = true;
|
|
114
|
+
ShowVolume = true;
|
|
115
|
+
/**
|
|
116
|
+
* Forwarded to the generic player. On by default — the streaming URL is same-origin (served by
|
|
117
|
+
* MJAPI) and a no-Range GET returns the full file, so client-side peak extraction decodes cleanly.
|
|
118
|
+
*/
|
|
119
|
+
ShowWaveform = true;
|
|
120
|
+
ShowFullscreen = true;
|
|
121
|
+
SkipSeconds = 30;
|
|
122
|
+
PlaybackRates = [0.5, 1, 1.25, 1.5, 2];
|
|
123
|
+
Autoplay = false;
|
|
124
|
+
StartAtMs = null;
|
|
125
|
+
/** Where the transcript sits. Defaults to `'bottom'` (matches the generic player). */
|
|
126
|
+
TranscriptPosition = 'bottom';
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// State
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
_initialized = false;
|
|
131
|
+
_resolvedTracks = [];
|
|
132
|
+
_isLoading = false;
|
|
133
|
+
_accessError = null;
|
|
134
|
+
_resolveToken = 0;
|
|
135
|
+
get ResolvedTracks() {
|
|
136
|
+
return this._resolvedTracks;
|
|
137
|
+
}
|
|
138
|
+
get IsLoading() {
|
|
139
|
+
return this._isLoading;
|
|
140
|
+
}
|
|
141
|
+
get AccessError() {
|
|
142
|
+
return this._accessError;
|
|
143
|
+
}
|
|
144
|
+
get HasFileIds() {
|
|
145
|
+
return this.collectFileIds().length > 0;
|
|
146
|
+
}
|
|
147
|
+
async ngOnInit() {
|
|
148
|
+
this._initialized = true;
|
|
149
|
+
await this.resolveTracks();
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Resolution
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/** Resolves all configured file ids into playable streaming tracks. Re-entrant-safe via a token. */
|
|
155
|
+
async resolveTracks() {
|
|
156
|
+
const ids = this.collectFileIds();
|
|
157
|
+
const token = ++this._resolveToken;
|
|
158
|
+
// start fresh
|
|
159
|
+
this._resolvedTracks = [];
|
|
160
|
+
this._accessError = null;
|
|
161
|
+
if (ids.length === 0) {
|
|
162
|
+
this._isLoading = false;
|
|
163
|
+
this.cdr.markForCheck();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this._isLoading = true;
|
|
167
|
+
this.cdr.markForCheck();
|
|
168
|
+
const tracks = [];
|
|
169
|
+
let firstError = null;
|
|
170
|
+
for (const id of ids) {
|
|
171
|
+
const result = await this.mintAccessToken(id);
|
|
172
|
+
if (token !== this._resolveToken) {
|
|
173
|
+
return; // a newer resolution superseded this one
|
|
174
|
+
}
|
|
175
|
+
if (result && result.Success && result.Url) {
|
|
176
|
+
tracks.push(this.buildTrack(id, result.Url, result.MimeType ?? null, result.Peaks ?? null));
|
|
177
|
+
}
|
|
178
|
+
else if (!firstError) {
|
|
179
|
+
firstError = result?.ErrorMessage || 'This recording is not available.';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (token !== this._resolveToken) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this._resolvedTracks = tracks;
|
|
186
|
+
// Only surface the no-access state when NOTHING resolved.
|
|
187
|
+
this._accessError = tracks.length === 0 ? firstError : null;
|
|
188
|
+
this._isLoading = false;
|
|
189
|
+
this.cdr.markForCheck();
|
|
190
|
+
}
|
|
191
|
+
/** Mints an authenticated streaming-URL token for one file id via the active provider. */
|
|
192
|
+
async mintAccessToken(fileId) {
|
|
193
|
+
try {
|
|
194
|
+
const provider = this.ProviderToUse;
|
|
195
|
+
const data = await provider.ExecuteGQL(CREATE_MEDIA_ACCESS_TOKEN_MUTATION, { fileId });
|
|
196
|
+
const payload = data?.CreateMediaAccessToken;
|
|
197
|
+
return payload ?? null;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
LogError(`CreateMediaAccessToken failed for file '${fileId}': ${err instanceof Error ? err.message : String(err)}`);
|
|
201
|
+
return { Success: false, ErrorMessage: 'Unable to load this recording.' };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Builds a MediaTrack from the streaming URL. Kind is MIME-derived (defaults to audio). When the
|
|
206
|
+
* server supplied precomputed waveform `peaks` (from a peaks.json sidecar), they're attached so the
|
|
207
|
+
* player renders the real waveform instantly with NO extra fetch/decode; otherwise the player
|
|
208
|
+
* extracts peaks client-side from the streaming URL.
|
|
209
|
+
*/
|
|
210
|
+
buildTrack(fileId, url, mimeType, peaks) {
|
|
211
|
+
const mime = mimeType || undefined;
|
|
212
|
+
const track = {
|
|
213
|
+
Id: fileId,
|
|
214
|
+
Kind: mime && mime.startsWith('video') ? 'video' : 'audio',
|
|
215
|
+
Url: url,
|
|
216
|
+
MimeType: mime,
|
|
217
|
+
};
|
|
218
|
+
if (Array.isArray(peaks) && peaks.length > 0) {
|
|
219
|
+
track.Peaks = peaks;
|
|
220
|
+
}
|
|
221
|
+
return track;
|
|
222
|
+
}
|
|
223
|
+
collectFileIds() {
|
|
224
|
+
const ids = [];
|
|
225
|
+
if (this._fileID) {
|
|
226
|
+
ids.push(this._fileID);
|
|
227
|
+
}
|
|
228
|
+
for (const id of this._fileIDs) {
|
|
229
|
+
if (id && !ids.includes(id)) {
|
|
230
|
+
ids.push(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return ids;
|
|
234
|
+
}
|
|
235
|
+
sameIds(a, b) {
|
|
236
|
+
if (a.length !== b.length) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return a.every((v, i) => v === b[i]);
|
|
240
|
+
}
|
|
241
|
+
static ɵfac = /*@__PURE__*/ (() => { let ɵMJStorageMediaPlayerComponent_BaseFactory; return function MJStorageMediaPlayerComponent_Factory(__ngFactoryType__) { return (ɵMJStorageMediaPlayerComponent_BaseFactory || (ɵMJStorageMediaPlayerComponent_BaseFactory = i0.ɵɵgetInheritedFactory(MJStorageMediaPlayerComponent)))(__ngFactoryType__ || MJStorageMediaPlayerComponent); }; })();
|
|
242
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MJStorageMediaPlayerComponent, selectors: [["mj-storage-media-player"]], inputs: { FileID: "FileID", FileIDs: "FileIDs", Transcript: "Transcript", ShowTranscript: "ShowTranscript", ShowTranscriptToggle: "ShowTranscriptToggle", ShowSpeedControl: "ShowSpeedControl", ShowSkipControls: "ShowSkipControls", ShowVolume: "ShowVolume", ShowWaveform: "ShowWaveform", ShowFullscreen: "ShowFullscreen", SkipSeconds: "SkipSeconds", PlaybackRates: "PlaybackRates", Autoplay: "Autoplay", StartAtMs: "StartAtMs", TranscriptPosition: "TranscriptPosition" }, features: [i0.ɵɵInheritDefinitionFeature], decls: 4, vars: 1, consts: [["role", "status", "aria-live", "polite", 1, "mj-storage-media-state"], ["role", "alert", 1, "mj-storage-media-state", "mj-storage-media-state--error"], ["role", "status", 1, "mj-storage-media-state"], [3, "Tracks", "Transcript", "ShowTranscript", "ShowTranscriptToggle", "ShowSpeedControl", "ShowSkipControls", "ShowVolume", "ShowWaveform", "ShowFullscreen", "SkipSeconds", "PlaybackRates", "Autoplay", "StartAtMs", "TranscriptPosition"], ["aria-hidden", "true", 1, "fa-solid", "fa-circle-notch", "fa-spin", "mj-storage-media-state__icon"], ["aria-hidden", "true", 1, "fa-solid", "fa-lock", "mj-storage-media-state__icon"], [1, "mj-storage-media-state__body"], [1, "mj-storage-media-state__title"], [1, "mj-storage-media-state__detail"], ["aria-hidden", "true", 1, "fa-solid", "fa-film", "mj-storage-media-state__icon"]], template: function MJStorageMediaPlayerComponent_Template(rf, ctx) { if (rf & 1) {
|
|
243
|
+
i0.ɵɵconditionalCreate(0, MJStorageMediaPlayerComponent_Conditional_0_Template, 4, 0, "div", 0)(1, MJStorageMediaPlayerComponent_Conditional_1_Template, 7, 1, "div", 1)(2, MJStorageMediaPlayerComponent_Conditional_2_Template, 4, 0, "div", 2)(3, MJStorageMediaPlayerComponent_Conditional_3_Template, 1, 14, "mj-media-player", 3);
|
|
244
|
+
} if (rf & 2) {
|
|
245
|
+
i0.ɵɵconditional(ctx.IsLoading ? 0 : ctx.AccessError ? 1 : !ctx.HasFileIds || ctx.ResolvedTracks.length === 0 ? 2 : 3);
|
|
246
|
+
} }, dependencies: [CommonModule, MJMediaPlayerComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n width: 100%;\n}\n\n.mj-storage-media-state[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 1.5rem 1rem;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.mj-storage-media-state__icon[_ngcontent-%COMP%] {\n font-size: 1.25rem;\n}\n\n.mj-storage-media-state--error[_ngcontent-%COMP%] {\n color: var(--mj-status-error-text);\n background: var(--mj-status-error-bg);\n border-color: var(--mj-status-error-border);\n}\n\n.mj-storage-media-state__body[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n min-width: 0;\n}\n\n.mj-storage-media-state__title[_ngcontent-%COMP%] {\n font-weight: 600;\n}\n\n.mj-storage-media-state__detail[_ngcontent-%COMP%] {\n font-size: 0.8125rem;\n opacity: 0.85;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fa-spin[_ngcontent-%COMP%] {\n animation: none;\n }\n}"], changeDetection: 0 });
|
|
247
|
+
}
|
|
248
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJStorageMediaPlayerComponent, [{
|
|
249
|
+
type: Component,
|
|
250
|
+
args: [{ selector: 'mj-storage-media-player', standalone: true, imports: [CommonModule, MJMediaPlayerComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (IsLoading) {\n <div class=\"mj-storage-media-state\" role=\"status\" aria-live=\"polite\">\n <i class=\"fa-solid fa-circle-notch fa-spin mj-storage-media-state__icon\" aria-hidden=\"true\"></i>\n <span>Loading recording…</span>\n </div>\n} @else if (AccessError) {\n <div class=\"mj-storage-media-state mj-storage-media-state--error\" role=\"alert\">\n <i class=\"fa-solid fa-lock mj-storage-media-state__icon\" aria-hidden=\"true\"></i>\n <div class=\"mj-storage-media-state__body\">\n <span class=\"mj-storage-media-state__title\">This recording isn't available \u2014 you may not have access.</span>\n <span class=\"mj-storage-media-state__detail\">{{ AccessError }}</span>\n </div>\n </div>\n} @else if (!HasFileIds || ResolvedTracks.length === 0) {\n <div class=\"mj-storage-media-state\" role=\"status\">\n <i class=\"fa-solid fa-film mj-storage-media-state__icon\" aria-hidden=\"true\"></i>\n <span>No recording to play.</span>\n </div>\n} @else {\n <mj-media-player\n [Tracks]=\"ResolvedTracks\"\n [Transcript]=\"Transcript\"\n [ShowTranscript]=\"ShowTranscript\"\n [ShowTranscriptToggle]=\"ShowTranscriptToggle\"\n [ShowSpeedControl]=\"ShowSpeedControl\"\n [ShowSkipControls]=\"ShowSkipControls\"\n [ShowVolume]=\"ShowVolume\"\n [ShowWaveform]=\"ShowWaveform\"\n [ShowFullscreen]=\"ShowFullscreen\"\n [SkipSeconds]=\"SkipSeconds\"\n [PlaybackRates]=\"PlaybackRates\"\n [Autoplay]=\"Autoplay\"\n [StartAtMs]=\"StartAtMs\"\n [TranscriptPosition]=\"TranscriptPosition\"\n ></mj-media-player>\n}\n", styles: [":host {\n display: block;\n width: 100%;\n}\n\n.mj-storage-media-state {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 1.5rem 1rem;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.mj-storage-media-state__icon {\n font-size: 1.25rem;\n}\n\n.mj-storage-media-state--error {\n color: var(--mj-status-error-text);\n background: var(--mj-status-error-bg);\n border-color: var(--mj-status-error-border);\n}\n\n.mj-storage-media-state__body {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n min-width: 0;\n}\n\n.mj-storage-media-state__title {\n font-weight: 600;\n}\n\n.mj-storage-media-state__detail {\n font-size: 0.8125rem;\n opacity: 0.85;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fa-spin {\n animation: none;\n }\n}\n"] }]
|
|
251
|
+
}], null, { FileID: [{
|
|
252
|
+
type: Input
|
|
253
|
+
}], FileIDs: [{
|
|
254
|
+
type: Input
|
|
255
|
+
}], Transcript: [{
|
|
256
|
+
type: Input
|
|
257
|
+
}], ShowTranscript: [{
|
|
258
|
+
type: Input
|
|
259
|
+
}], ShowTranscriptToggle: [{
|
|
260
|
+
type: Input
|
|
261
|
+
}], ShowSpeedControl: [{
|
|
262
|
+
type: Input
|
|
263
|
+
}], ShowSkipControls: [{
|
|
264
|
+
type: Input
|
|
265
|
+
}], ShowVolume: [{
|
|
266
|
+
type: Input
|
|
267
|
+
}], ShowWaveform: [{
|
|
268
|
+
type: Input
|
|
269
|
+
}], ShowFullscreen: [{
|
|
270
|
+
type: Input
|
|
271
|
+
}], SkipSeconds: [{
|
|
272
|
+
type: Input
|
|
273
|
+
}], PlaybackRates: [{
|
|
274
|
+
type: Input
|
|
275
|
+
}], Autoplay: [{
|
|
276
|
+
type: Input
|
|
277
|
+
}], StartAtMs: [{
|
|
278
|
+
type: Input
|
|
279
|
+
}], TranscriptPosition: [{
|
|
280
|
+
type: Input
|
|
281
|
+
}] }); })();
|
|
282
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MJStorageMediaPlayerComponent, { className: "MJStorageMediaPlayerComponent", filePath: "src/lib/storage-media-player/storage-media-player.component.ts", lineNumber: 72 }); })();
|
|
283
|
+
//# sourceMappingURL=storage-media-player.component.js.map
|