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.
npm install @memberjunction/ng-media-player
This package ships two standalone components:
| Component | Selector | Purpose |
|---|---|---|
MJMediaPlayerComponent |
mj-media-player |
Generic, framework-agnostic player. You give it one or more MediaTracks (plain URLs) and an optional transcript. |
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. |
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.
flowchart TD
subgraph Wrapper["MJStorageMediaPlayerComponent (mj-storage-media-player)"]
F["FileID / FileIDs[]"] --> M["CreateMediaAccessToken mutation
(per-user permission check)"]
M --> U["/media/:fileId?token= streaming URL
+ optional peaks.json peaks"]
end
subgraph Generic["MJMediaPlayerComponent (mj-media-player)"]
T["MediaTrack[]"] --> P["Transport bar + waveform scrubber"]
T --> W["Real waveform (supplied Peaks OR client-decoded)"]
C["MediaTranscriptCue[]"] --> X["Click-to-seek transcript (auto-highlight)"]
end
U -->|"MediaTrack[] (Url + Peaks)"| Generic
style Wrapper fill:#2d6a9f,stroke:#1a4971,color:#fff
style Generic fill:#2d8659,stroke:#1a5c3a,color:#fff
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.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.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.role="slider" with arrow/Home/End support.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).Play(), Pause(), SeekToMs(), SetPlaybackRate(), SeekToCue(), etc.import { MJMediaPlayerComponent, MediaTrack, MediaTranscriptCue } from '@memberjunction/ng-media-player';
@Component({
standalone: true,
imports: [MJMediaPlayerComponent],
template: `
<mj-media-player
[Tracks]="tracks"
[Transcript]="transcript"
(Ended)="onEnded()">
</mj-media-player>
`,
})
export class MyComponent {
tracks: MediaTrack[] = [
{ Id: 'a1', Kind: 'audio', Url: 'https://example.com/interview.wav' },
];
transcript: MediaTranscriptCue[] = [
{ Id: 'c1', StartMs: 0, SpeakerLabel: 'Host', Text: 'Welcome to the show.' },
{ Id: 'c2', StartMs: 3200, SpeakerLabel: 'Guest', Text: 'Thanks for having me.' },
];
onEnded() { /* ... */ }
}
import { MJStorageMediaPlayerComponent } from '@memberjunction/ng-media-player';
@Component({
standalone: true,
imports: [MJStorageMediaPlayerComponent],
template: `
<mj-storage-media-player
[FileID]="recordingFileId"
[Transcript]="transcript">
</mj-storage-media-player>
`,
})
export class RecordingPlaybackComponent {
recordingFileId = '...'; // an MJ: Files row ID
transcript: MediaTranscriptCue[] = [ /* ... */ ];
}
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.
MJMediaPlayerComponent (mj-media-player)| Property | Type | Default | Description |
|---|---|---|---|
Tracks |
MediaTrack[] |
[] |
The track(s) to play. One audio track → transport bar + waveform; many videos → grid. |
Transcript |
MediaTranscriptCue[] | null |
null |
Transcript cues. When set (and ShowTranscript), a transcript panel renders. |
Autoplay |
boolean |
false |
Begin playback automatically once media is ready. |
StartAtMs |
number | null |
null |
Start playback at this position (ms) once metadata loads. |
ShowTranscript |
boolean |
true |
Master switch for the transcript panel (when a transcript is provided). |
ShowTranscriptToggle |
boolean |
true |
Whether the transcript show/hide toggle renders in the transport bar. |
ShowSpeedControl |
boolean |
true |
Whether the playback-rate menu is shown. |
ShowSkipControls |
boolean |
true |
Whether the ±skip buttons are shown. |
ShowVolume |
boolean |
true |
Whether the volume slider + mute are shown. |
ShowWaveform |
boolean |
true |
Whether the real audio waveform (which doubles as the scrubber) is shown for audio-only tracks. |
WaveformBarCount |
number |
DEFAULT_WAVEFORM_BARS |
Number of bars the waveform renders / downsamples to. |
ShowFullscreen |
boolean |
true |
Whether the fullscreen button is shown for video. |
SkipSeconds |
number |
30 |
Seconds the ±skip buttons jump. |
PlaybackRates |
number[] |
[0.5, 1, 1.25, 1.5, 2] |
Rates offered in the rate menu. |
InitialRate |
number |
1 |
Initial playback rate. |
InitialVolume |
number |
1 |
Initial volume (0..1). |
TranscriptPosition |
'bottom' | 'side' |
'bottom' |
Where the transcript panel sits relative to the media. |
Before* events are cancelable — set event.Cancel = true in a handler to abort.
| Event | Type | Description |
|---|---|---|
BeforePlay |
EventEmitter<MediaPlayerCancelableEvent> |
Before playback starts (cancelable). |
AfterPlay |
EventEmitter<void> |
After playback starts. |
BeforePause |
EventEmitter<MediaPlayerCancelableEvent> |
Before pause (cancelable). |
AfterPause |
EventEmitter<void> |
After pause. |
BeforeSeek |
EventEmitter<MediaSeekEvent> |
Before a seek (cancelable; carries FromMs/ToMs). |
AfterSeek |
EventEmitter<number> |
After a seek (the new position in ms). |
BeforeRateChange |
EventEmitter<MediaRateEvent> |
Before a rate change (cancelable; carries FromRate/ToRate). |
AfterRateChange |
EventEmitter<number> |
After a rate change (the new rate). |
CueActivated |
EventEmitter<MediaCueEvent> |
When the active transcript cue changes during playback. |
CueClicked |
EventEmitter<MediaCueEvent> |
When a transcript cue is clicked. |
TimeUpdate |
EventEmitter<number> |
On every time update (current position in ms). |
DurationChange |
EventEmitter<number> |
When the media duration is resolved/changes (ms). |
Ended |
EventEmitter<void> |
When playback reaches the end. |
StateChanged |
EventEmitter<MediaPlaybackState> |
On every playback-lifecycle transition. |
TranscriptVisibilityChanged |
EventEmitter<boolean> |
When the transcript is toggled (true = shown). |
| Method | Returns | Description |
|---|---|---|
Play() |
void |
Begin playback (honors BeforePlay). |
Pause() |
void |
Pause playback (honors BeforePause). |
TogglePlay() |
void |
Toggle play/pause. |
SeekToMs(ms) |
void |
Seek to an absolute position (honors BeforeSeek). |
SkipForward() / SkipBackward() |
void |
Skip ±SkipSeconds. |
SetPlaybackRate(rate) |
void |
Set the playback rate (honors BeforeRateChange). |
SeekToCue(index) |
void |
Seek to a transcript cue's start (continues playing if playing, repositions if paused). |
SetVolume(v) |
void |
Set volume 0..1 (unmutes if > 0). |
ToggleMute() |
void |
Toggle mute. |
EnterFullscreen() / ExitFullscreen() / ToggleFullscreen() |
void |
Fullscreen control for video. |
ToggleTranscript() |
void |
Show/hide the transcript panel (emits TranscriptVisibilityChanged). |
Read-only getters expose live state for template binding / inspection, including CurrentTimeMs, DurationMs, IsPlaying, PlaybackRate, ActiveCueIndex, MediaState, IsLoading, IsBuffering, and StatusLabel.
MJStorageMediaPlayerComponent (mj-storage-media-player)Extends BaseAngularComponent, so it accepts the standard [Provider] input for multi-provider apps.
| Property | Type | Default | Description |
|---|---|---|---|
FileID |
string | null |
null |
Convenience single MJ: Files id. Combined with FileIDs; duplicates are de-duped. |
FileIDs |
string[] |
[] |
Multi-track file ids (e.g. separate meeting streams). |
Transcript |
MediaTranscriptCue[] | null |
null |
Transcript cues forwarded to the generic player. |
ShowTranscript / ShowTranscriptToggle / ShowSpeedControl / ShowSkipControls / ShowVolume / ShowWaveform / ShowFullscreen |
boolean |
true |
Pass-through feature flags, forwarded to the generic player. |
SkipSeconds |
number |
30 |
Forwarded skip step. |
PlaybackRates |
number[] |
[0.5, 1, 1.25, 1.5, 2] |
Forwarded rate menu. |
Autoplay |
boolean |
false |
Forwarded autoplay flag. |
StartAtMs |
number | null |
null |
Forwarded start position. |
TranscriptPosition |
'side' | 'bottom' |
'bottom' |
Forwarded transcript position. |
| Getter | Type | Description |
|---|---|---|
ResolvedTracks |
MediaTrack[] |
The streaming tracks resolved from the file ids. |
IsLoading |
boolean |
True while minting access tokens / resolving tracks. |
AccessError |
string | null |
A no-access / unavailable message, set only when nothing resolved. |
HasFileIds |
boolean |
Whether any file id is configured. |
The wrapper resolves each file id by calling the CreateMediaAccessToken GraphQL mutation (server-side, in @memberjunction/server). The server:
GET /media/:fileId?token=….peaks.json sidecar sits beside the recording in storage, returns its precomputed waveform peaks alongside the URL.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).
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.
type MediaKind = 'audio' | 'video';
type MediaPlaybackState =
| 'idle' | 'loading' | 'buffering' | 'ready'
| 'playing' | 'paused' | 'ended' | 'error';
interface MediaTrack {
Id: string; // stable identifier (track-by + event correlation)
Kind: MediaKind; // audio vs. video
Url: string; // http(s) or blob: object URL
MimeType?: string; // optional MIME hint (e.g. 'audio/wav', 'video/mp4')
Label?: string; // optional label shown in multi-track layouts
PosterUrl?: string; // optional poster image for video
Peaks?: number[]; // optional precomputed waveform peaks (normalized 0..1, one per bar).
// Supplied → rendered directly, NO client-side decode.
// Absent (audio + waveform on) → player decodes the URL client-side.
}
interface MediaTranscriptCue {
Id: string; // stable cue identifier
StartMs: number; // cue start (ms from media start)
EndMs?: number; // optional cue end; when absent, the next cue's start is used
SpeakerLabel?: string; // optional speaker label (color-coded)
Text: string; // the spoken text
}
interface MediaPlayerCancelableEvent { Cancel: boolean; } // set true to abort
interface MediaSeekEvent extends MediaPlayerCancelableEvent { FromMs: number; ToMs: number; }
interface MediaRateEvent extends MediaPlayerCancelableEvent { FromRate: number; ToRate: number; }
interface MediaCueEvent { Cue: MediaTranscriptCue; Index: number; }
Helper utilities (computeActiveCueIndex, downsamplePeaks, nextPlaybackState, transcript-layout resolvers) are also exported from the package public API for reuse and testing.
@angular/common / @angular/core (peer)@memberjunction/core, @memberjunction/graphql-dataprovider, @memberjunction/ng-base-types — used only by the storage wrapper. The generic MJMediaPlayerComponent has no MemberJunction dependency.npm run build # ngc
npm run test # vitest
ISC