videojs-rewind 0.1.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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Core plugin class for videojs-rewind.
3
+ *
4
+ * Extends video.js's advanced plugin pattern (class-based).
5
+ * Manages lifecycle, state, custom DOM injection, and coordinates
6
+ * all sub-components: seek bar, rewind-to-VOD, quality, storyboard,
7
+ * hover tooltips, toast notifications, and keyboard seek.
8
+ */
9
+ import type { RewindPluginOptions, RewindState, PluginDomElements } from './types.js';
10
+ declare const RewindPlugin_base: new (...args: any[]) => any;
11
+ /**
12
+ * @class RewindPlugin
13
+ * @extends Plugin
14
+ */
15
+ declare class RewindPlugin extends RewindPlugin_base {
16
+ options: RewindPluginOptions;
17
+ state: RewindState;
18
+ dom: PluginDomElements;
19
+ private _tickInterval;
20
+ private _cdns;
21
+ private _storyboardInfo;
22
+ private _spriteImages;
23
+ private _qualitySuppressionTimer;
24
+ private _toastTimer;
25
+ private _keydownHandler;
26
+ private _wasPlaying;
27
+ /**
28
+ * @param player - The video.js player instance
29
+ * @param options - Partial plugin options (merged with defaults via Zod)
30
+ */
31
+ constructor(player: any, options: Partial<RewindPluginOptions>);
32
+ /** Called once the player is ready. Sets up DOM, listeners, and VHS hooks. */
33
+ private _onPlayerReady;
34
+ /**
35
+ * Clean up all resources. Called when the plugin is disposed
36
+ * or the player is destroyed.
37
+ */
38
+ dispose(): void;
39
+ /** Build and inject custom DOM elements into the player's control bar. */
40
+ private _buildDOM;
41
+ /** Remove all injected DOM elements. */
42
+ private _removeDOM;
43
+ /**
44
+ * Create a DOM element with attributes and optional text content.
45
+ */
46
+ private _createEl;
47
+ private _responseHook;
48
+ private _wireVhsHooks;
49
+ private _unwireVhsHooks;
50
+ /**
51
+ * Get the CDN base URL for a given playlist URI.
52
+ */
53
+ private _getCdnBaseUrl;
54
+ private _wireKeyboardSeek;
55
+ private _onLoadedMetadata;
56
+ private _onLoadStart;
57
+ private _onDurationChange;
58
+ private _onSeeked;
59
+ private _onQualityLevelsChange;
60
+ private _onSeekMouseDown;
61
+ private _onSeekMouseMove;
62
+ private _onSeekMouseUp;
63
+ private _onGoLive;
64
+ private _showHoverTime;
65
+ private _hideHoverTime;
66
+ /**
67
+ * Show a toast notification message.
68
+ * @param message - The message to display
69
+ * @param duration - Duration in ms (default: 3000)
70
+ */
71
+ private _showToast;
72
+ private _startTick;
73
+ private _updateDisplay;
74
+ /**
75
+ * Update the seek slider fill and handle position.
76
+ */
77
+ private _updateSlider;
78
+ private _checkRewindToVod;
79
+ private _rewindToVod;
80
+ /**
81
+ * Swap back to the live source.
82
+ */
83
+ private _goLive;
84
+ private _applyQualityConstraint;
85
+ /**
86
+ * Start the ABR suppression window.
87
+ * During this window, quality constraint is not enforced to let ABR ramp up.
88
+ */
89
+ private _startQualitySuppression;
90
+ /**
91
+ * Load quality preference from localStorage.
92
+ */
93
+ private _loadQualityPreference;
94
+ /**
95
+ * Save quality preference to localStorage.
96
+ */
97
+ private _saveQualityPreference;
98
+ /**
99
+ * Set the preferred quality level.
100
+ * @param quality - Quality height (e.g., 1080) or 'auto'
101
+ */
102
+ setQuality(quality: number | 'auto'): void;
103
+ /**
104
+ * Get the current quality level.
105
+ */
106
+ getQuality(): number | 'auto';
107
+ /**
108
+ * Fetch storyboard metadata for a VOD and preload sprite images.
109
+ */
110
+ private _fetchStoryboardInfo;
111
+ /**
112
+ * Preload all sprite sheet images.
113
+ */
114
+ private _preloadSpriteImages;
115
+ /**
116
+ * Show storyboard canvas at the initial seek position.
117
+ */
118
+ private _showStoryboardAt;
119
+ /**
120
+ * Draw a storyboard frame on the canvas for the given seek time.
121
+ */
122
+ private _drawStoryboardFrame;
123
+ /**
124
+ * Hide the storyboard canvas.
125
+ */
126
+ private _hideStoryboard;
127
+ /**
128
+ * Format seconds into HH:MM:SS or MM:SS.
129
+ */
130
+ _formatTime(seconds: number): string;
131
+ /**
132
+ * Extract channel name from the current source URL.
133
+ */
134
+ private _getChannelName;
135
+ }
136
+ export default RewindPlugin;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Quality Constraint for videojs-rewind.
3
+ *
4
+ * Enforces a user-preferred quality level across source swaps (live ↔ VOD).
5
+ * Integrates with videojs-contrib-quality-levels and VHS fastQualityChange_.
6
+ */
7
+ /**
8
+ * Constrain quality levels to a preferred height.
9
+ * Disables all levels that don't match the preferred height.
10
+ * @param player - The video.js player
11
+ * @param preferredHeight - Target height (e.g., 1080)
12
+ */
13
+ export declare function constrainQuality(player: any, preferredHeight: number): void;
14
+ /**
15
+ * Suppression window duration in ms after loadstart.
16
+ * During this window, the quality constraint is not enforced
17
+ * to avoid feedback loops with the ABR ramp-up.
18
+ */
19
+ export declare const SUPPRESSION_WINDOW_MS = 6000;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Rewind-to-VOD Flow for videojs-rewind.
3
+ *
4
+ * Detects when the user seeks behind the DVR window and orchestrates
5
+ * the source swap from live HLS to VOD HLS.
6
+ *
7
+ * State machine: idle → resolving → swapping → seeking → playing
8
+ */
9
+ /**
10
+ * Check if a seek time falls behind the DVR window.
11
+ * @param player - The video.js player
12
+ * @param seekTime - Desired seek time in seconds
13
+ * @param threshold - DVR window threshold in seconds
14
+ * @returns True if seek time is behind DVR window
15
+ */
16
+ export declare function detectBehindDvr(player: any, seekTime: number, threshold: number): boolean;
17
+ /**
18
+ * State machine states for rewind-to-VOD flow.
19
+ */
20
+ export declare const RewindState: {
21
+ readonly IDLE: 'idle';
22
+ readonly RESOLVING: 'resolving';
23
+ readonly SWAPPING: 'swapping';
24
+ readonly SEEKING: 'seeking';
25
+ readonly PLAYING: 'playing';
26
+ readonly ERROR: 'error';
27
+ };
28
+ export type RewindStateValue = (typeof RewindState)[keyof typeof RewindState];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Zod schemas for videojs-rewind options validation.
3
+ *
4
+ * Uses Zod 4 pattern for schema definitions.
5
+ * Provides runtime validation of plugin options with descriptive error messages.
6
+ */
7
+ import { z } from 'zod';
8
+ /** Full plugin options schema */
9
+ export declare const RewindPluginOptionsSchema: z.ZodObject<{
10
+ customSeekBar: z.ZodDefault<z.ZodBoolean>;
11
+ showTotalTime: z.ZodDefault<z.ZodBoolean>;
12
+ enableRewindToVod: z.ZodDefault<z.ZodBoolean>;
13
+ resolveVodUrl: z.ZodDefault<z.ZodNullable<z.ZodCustom<(channelName: string, seekTime: number) => Promise<{
14
+ vodId: string;
15
+ hlsUrl: string;
16
+ } | null>, (channelName: string, seekTime: number) => Promise<{
17
+ vodId: string;
18
+ hlsUrl: string;
19
+ } | null>>>>;
20
+ resolveLiveUrl: z.ZodDefault<z.ZodNullable<z.ZodCustom<(channelName: string) => Promise<string | null>, (channelName: string) => Promise<string | null>>>>;
21
+ preferredQuality: z.ZodDefault<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"auto">]>>;
22
+ persistQuality: z.ZodDefault<z.ZodBoolean>;
23
+ storageKeyPrefix: z.ZodDefault<z.ZodString>;
24
+ storyboard: z.ZodOptional<z.ZodObject<{
25
+ fetchInfo: z.ZodOptional<z.ZodCustom<(vodId: string, cdnBaseUrl: string) => Promise<Record<string, unknown> | null>, (vodId: string, cdnBaseUrl: string) => Promise<Record<string, unknown> | null>>>;
26
+ previewWidth: z.ZodDefault<z.ZodNumber>;
27
+ previewHeight: z.ZodDefault<z.ZodNumber>;
28
+ }, z.core.$strict>>;
29
+ dvrWindowThreshold: z.ZodDefault<z.ZodNumber>;
30
+ showToasts: z.ZodDefault<z.ZodBoolean>;
31
+ }, z.core.$strict>;
32
+ /** Type for the resolved options after schema parsing */
33
+ export type ResolvedRewindPluginOptions = z.output<typeof RewindPluginOptionsSchema>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Seek Bar Component for videojs-rewind.
3
+ *
4
+ * Pure functions for seek bar calculations:
5
+ * - calculateSeekTime: map pixel offset to seek time
6
+ * - sliderFillPercent: compute fill percentage for display
7
+ */
8
+ /**
9
+ * Calculate seek time from mouse/touch position relative to seek bar.
10
+ * @param offsetX - Mouse X offset from seek bar left edge
11
+ * @param outerWidth - Total width of seek bar
12
+ * @param maxTime - Current max time (live edge or VOD duration)
13
+ * @returns Seek time in seconds, clamped to [0, maxTime]
14
+ */
15
+ export declare function calculateSeekTime(offsetX: number, outerWidth: number, maxTime: number): number;
16
+ /**
17
+ * Calculate slider fill percentage.
18
+ * @param currentTime - Current playback time
19
+ * @param maxTime - Max time
20
+ * @returns Percentage (0-100)
21
+ */
22
+ export declare function sliderFillPercent(currentTime: number, maxTime: number): number;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Storyboard / Seek Thumbnail for videojs-rewind.
3
+ *
4
+ * Pure functions for sprite-sheet-based seek thumbnail previews:
5
+ * - calculateFrameIndex: map seek time to frame index
6
+ * - calculateCellPosition: derive sprite sheet cell coordinates
7
+ */
8
+ /** Sprite sheet cell position */
9
+ export interface CellPosition {
10
+ sx: number;
11
+ sy: number;
12
+ sWidth: number;
13
+ sHeight: number;
14
+ }
15
+ /**
16
+ * Calculate the frame index for a given seek time.
17
+ * @param seekTime - Seek time in seconds
18
+ * @param interval - Seconds per frame
19
+ * @returns Frame index
20
+ */
21
+ export declare function calculateFrameIndex(seekTime: number, interval: number): number;
22
+ /**
23
+ * Calculate the sprite sheet cell position for a given frame index.
24
+ * @param frameIndex - Frame index
25
+ * @param cols - Number of columns in sprite sheet
26
+ * @param frameWidth - Width of each frame in pixels
27
+ * @param frameHeight - Height of each frame in pixels
28
+ * @returns Cell position and dimensions for drawImage()
29
+ */
30
+ export declare function calculateCellPosition(frameIndex: number, cols: number, frameWidth: number, frameHeight: number): CellPosition;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Type definitions for videojs-rewind.
3
+ */
4
+ import type { z } from 'zod';
5
+ import type { RewindPluginOptionsSchema } from './schema.js';
6
+ /** Inferred plugin options type from the Zod schema */
7
+ export type RewindPluginOptions = z.infer<typeof RewindPluginOptionsSchema>;
8
+ /** Storyboard sprite sheet metadata */
9
+ export interface StoryboardInfo {
10
+ width: number;
11
+ height: number;
12
+ cols: number;
13
+ rows: number;
14
+ interval: number;
15
+ count: number;
16
+ images: string[];
17
+ }
18
+ /** Resolved VOD result from the resolveVodUrl callback */
19
+ export interface VodResult {
20
+ vodId: string;
21
+ hlsUrl: string;
22
+ }
23
+ /** DVR window range */
24
+ export interface DvrWindow {
25
+ start: number;
26
+ end: number;
27
+ }
28
+ /** Internal plugin state */
29
+ export interface RewindState {
30
+ videoMode: 'live' | 'vod';
31
+ maxTime: number;
32
+ vodMaxTime: number;
33
+ isTransitioning: boolean;
34
+ currentQuality: number | 'auto';
35
+ atLiveEdge: boolean;
36
+ dvrWindow: DvrWindow;
37
+ }
38
+ /** Quality level from videojs-contrib-quality-levels */
39
+ export interface QualityLevel {
40
+ id: string;
41
+ label: string;
42
+ width: number;
43
+ height: number;
44
+ bitrate: number;
45
+ enabled: boolean;
46
+ }
47
+ /** DOM element references managed by the plugin */
48
+ export interface PluginDomElements {
49
+ seekOuter: HTMLDivElement | null;
50
+ seekSlider: HTMLDivElement | null;
51
+ seekHandle: HTMLDivElement | null;
52
+ buffered: HTMLDivElement | null;
53
+ timer: HTMLSpanElement | null;
54
+ timerDivider: HTMLSpanElement | null;
55
+ timerEnd: HTMLSpanElement | null;
56
+ liveButton: HTMLButtonElement | null;
57
+ storyboardCanvas: HTMLCanvasElement | null;
58
+ hoverTime: HTMLDivElement | null;
59
+ toastContainer: HTMLDivElement | null;
60
+ }
@@ -0,0 +1,221 @@
1
+ /* ==========================================================================
2
+ videojs-rewind — Custom DVR Seek Bar, Timer, and Storyboard Styles
3
+ All styles scoped under .vjs-rewind-active to avoid conflicts
4
+ with other plugins and skins.
5
+ ========================================================================== */
6
+
7
+ /* ---------------------------------------------------------------------------
8
+ Timer Display
9
+ --------------------------------------------------------------------------- */
10
+
11
+ .vjs-rewind-active .vjs-rewind-timer {
12
+ display: inline-block;
13
+ font-family: monospace;
14
+ font-size: 1.1em;
15
+ color: #fff;
16
+ padding: 0 0.5em;
17
+ line-height: 3em;
18
+ vertical-align: middle;
19
+ cursor: default;
20
+ user-select: none;
21
+ }
22
+
23
+ .vjs-rewind-active .vjs-rewind-timer-divider {
24
+ display: inline-block;
25
+ font-family: monospace;
26
+ font-size: 1.1em;
27
+ color: #aaa;
28
+ padding: 0 0.2em;
29
+ line-height: 3em;
30
+ vertical-align: middle;
31
+ user-select: none;
32
+ }
33
+
34
+ .vjs-rewind-active .vjs-rewind-timer-end {
35
+ display: inline-block;
36
+ font-family: monospace;
37
+ font-size: 1.1em;
38
+ color: #aaa;
39
+ padding: 0 0.5em 0 0;
40
+ line-height: 3em;
41
+ vertical-align: middle;
42
+ cursor: default;
43
+ user-select: none;
44
+ }
45
+
46
+ /* ---------------------------------------------------------------------------
47
+ Seek Bar
48
+ --------------------------------------------------------------------------- */
49
+
50
+ .vjs-rewind-active .vjs-rewind-seek-outer {
51
+ position: relative;
52
+ flex: 1;
53
+ height: 0.5em;
54
+ margin: 0 0.5em;
55
+ background: #333;
56
+ border-radius: 0.25em;
57
+ cursor: pointer;
58
+ align-self: center;
59
+ z-index: 1;
60
+ }
61
+
62
+ .vjs-rewind-active .vjs-rewind-slider {
63
+ position: absolute;
64
+ top: 0;
65
+ left: 0;
66
+ height: 100%;
67
+ background: #f60;
68
+ border-radius: 0.25em;
69
+ transition: width 0.1s linear;
70
+ pointer-events: none;
71
+ }
72
+
73
+ .vjs-rewind-active .vjs-rewind-handle {
74
+ position: absolute;
75
+ top: -0.3em;
76
+ width: 1em;
77
+ height: 1em;
78
+ background: #f60;
79
+ border-radius: 50%;
80
+ transform: translateX(-50%);
81
+ transition: left 0.1s linear;
82
+ pointer-events: none;
83
+ z-index: 2;
84
+ }
85
+
86
+ .vjs-rewind-active .vjs-rewind-buffered {
87
+ position: absolute;
88
+ top: 0;
89
+ left: 0;
90
+ height: 100%;
91
+ background: rgba(255, 255, 255, 0.2);
92
+ border-radius: 0.25em;
93
+ pointer-events: none;
94
+ }
95
+
96
+ /* ---------------------------------------------------------------------------
97
+ Live Button
98
+ --------------------------------------------------------------------------- */
99
+
100
+ .vjs-rewind-active .vjs-rewind-live-button {
101
+ display: none;
102
+ font-family: monospace;
103
+ font-size: 0.9em;
104
+ color: #fff;
105
+ background: #f60;
106
+ border: none;
107
+ border-radius: 0.25em;
108
+ padding: 0.25em 0.75em;
109
+ cursor: pointer;
110
+ line-height: 2em;
111
+ vertical-align: middle;
112
+ margin: 0 0.5em;
113
+ }
114
+
115
+ .vjs-rewind-active .vjs-rewind-live-button:hover {
116
+ background: #e55;
117
+ }
118
+
119
+ /* ---------------------------------------------------------------------------
120
+ Storyboard Canvas
121
+ --------------------------------------------------------------------------- */
122
+
123
+ .vjs-rewind-active .vjs-rewind-storyboard {
124
+ position: absolute;
125
+ bottom: 100%;
126
+ left: 50%;
127
+ transform: translateX(-50%);
128
+ margin-bottom: 0.5em;
129
+ border: 2px solid #fff;
130
+ border-radius: 0.25em;
131
+ box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
132
+ background: #000;
133
+ pointer-events: none;
134
+ z-index: 10;
135
+ }
136
+
137
+ /* ---------------------------------------------------------------------------
138
+ State: Scrubbing (hide native controls)
139
+ --------------------------------------------------------------------------- */
140
+
141
+ .vjs-rewind-active.vjs-scrubbing .vjs-progress-control {
142
+ display: none;
143
+ }
144
+
145
+ /* ---------------------------------------------------------------------------
146
+ Responsive: Hide timer on small players
147
+ --------------------------------------------------------------------------- */
148
+
149
+ @media (max-width: 600px) {
150
+ .vjs-rewind-active .vjs-rewind-timer-divider,
151
+ .vjs-rewind-active .vjs-rewind-timer-end {
152
+ display: none;
153
+ }
154
+ }
155
+
156
+ /* ---------------------------------------------------------------------------
157
+ Transitioning State
158
+ --------------------------------------------------------------------------- */
159
+
160
+ .vjs-rewind-active.vjs-rewind-transitioning .vjs-rewind-seek-outer {
161
+ opacity: 0.5;
162
+ pointer-events: none;
163
+ }
164
+
165
+ /* ---------------------------------------------------------------------------
166
+ Hover Time Tooltip
167
+ --------------------------------------------------------------------------- */
168
+
169
+ .vjs-rewind-active .vjs-rewind-hover-time {
170
+ position: absolute;
171
+ bottom: 100%;
172
+ font-family: monospace;
173
+ font-size: 0.85em;
174
+ color: #fff;
175
+ background: rgba(0, 0, 0, 0.75);
176
+ padding: 0.2em 0.5em;
177
+ border-radius: 0.25em;
178
+ pointer-events: none;
179
+ z-index: 10;
180
+ white-space: nowrap;
181
+ margin-bottom: 0.3em;
182
+ }
183
+
184
+ /* ---------------------------------------------------------------------------
185
+ Toast Notifications
186
+ --------------------------------------------------------------------------- */
187
+
188
+ .vjs-rewind-active .vjs-rewind-toast-container {
189
+ position: absolute;
190
+ bottom: 100%;
191
+ left: 50%;
192
+ transform: translateX(-50%);
193
+ margin-bottom: 0.5em;
194
+ pointer-events: none;
195
+ z-index: 15;
196
+ }
197
+
198
+ .vjs-rewind-active .vjs-rewind-toast {
199
+ font-family: monospace;
200
+ font-size: 0.85em;
201
+ color: #fff;
202
+ background: rgba(0, 0, 0, 0.8);
203
+ padding: 0.3em 0.75em;
204
+ border-radius: 0.25em;
205
+ white-space: nowrap;
206
+ text-align: center;
207
+ animation: vjs-rewind-toast-fadein 0.2s ease-in;
208
+ }
209
+
210
+ @keyframes vjs-rewind-toast-fadein {
211
+ from { opacity: 0; transform: translateY(0.3em); }
212
+ to { opacity: 1; transform: translateY(0); }
213
+ }
214
+
215
+ /* ---------------------------------------------------------------------------
216
+ Transitioning State
217
+ --------------------------------------------------------------------------- */
218
+
219
+ .vjs-rewind-active.vjs-rewind-transitioning {
220
+ cursor: wait;
221
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "videojs-rewind",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Full-stream seek bar, rewind-to-VOD, and quality enforcement for live DVR playback in video.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./dist/*": "./dist/*"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "style": "dist/videojs-rewind.css",
16
+ "files": [
17
+ "dist/"
18
+ ],
19
+ "scripts": {
20
+ "build": "bash scripts/build.sh",
21
+ "lint": "echo 'lint placeholder'",
22
+ "test": "bun test",
23
+ "prepublishOnly": "bun run build"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/ashleyjackson/videojs-rewind.git"
28
+ },
29
+ "keywords": [
30
+ "videojs",
31
+ "plugin",
32
+ "dvr",
33
+ "live",
34
+ "rewind",
35
+ "vod",
36
+ "hls",
37
+ "seek-bar"
38
+ ],
39
+ "author": "Ashley Jackson",
40
+ "license": "MIT",
41
+ "bugs": {
42
+ "url": "https://github.com/ashleyjackson/videojs-rewind/issues"
43
+ },
44
+ "homepage": "https://github.com/ashleyjackson/videojs-rewind#readme",
45
+ "peerDependencies": {
46
+ "typescript": "^7.0.0",
47
+ "video.js": "^8.23.9",
48
+ "@videojs/http-streaming": "^3.17.5",
49
+ "videojs-contrib-quality-levels": "^4.1.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "^1.3.14"
53
+ },
54
+ "dependencies": {
55
+ "global": "^4.4.0",
56
+ "zod": "^4.4.3"
57
+ }
58
+ }