@ldelia/react-media 0.9.0 → 0.11.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.
@@ -2,8 +2,14 @@ import { ReproductionBuilder } from './ReproductionBuilder';
2
2
  import { PlayAlongPlayer } from './Player/PlayAlongPlayer';
3
3
  import { YouTubePlayer } from './Player/YouTubePlayer';
4
4
  type Player = PlayAlongPlayer | YouTubePlayer;
5
+ export declare const REPRODUCTION_STATES: {
6
+ STOPPED: number;
7
+ COUNTING_IN: number;
8
+ PLAYING: number;
9
+ PAUSED: number;
10
+ };
11
+ type ReproductionState = (typeof REPRODUCTION_STATES)[keyof typeof REPRODUCTION_STATES];
5
12
  type Handler = (args: object) => void;
6
- declare const dispatchOnReadyHandlers: unique symbol;
7
13
  declare const dispatchOnSongStartHandlers: unique symbol;
8
14
  declare const dispatchOnCountingInHandlers: unique symbol;
9
15
  declare const dispatchOnPlayHandlers: unique symbol;
@@ -12,15 +18,14 @@ declare const dispatchOnPausedHandlers: unique symbol;
12
18
  declare const dispatchOnFinishHandlers: unique symbol;
13
19
  declare const dispatchOnErrorHandlers: unique symbol;
14
20
  export declare class Reproduction {
15
- private player;
16
- private requiresCountingIn;
17
- private songTempo;
21
+ private readonly player;
22
+ private readonly requiresCountingIn;
23
+ private readonly songTempo;
18
24
  private state;
19
25
  private interval;
20
26
  private loopInterval;
21
27
  private loopRange;
22
28
  private countingInCounter;
23
- private [dispatchOnReadyHandlers];
24
29
  private [dispatchOnSongStartHandlers];
25
30
  private [dispatchOnCountingInHandlers];
26
31
  private [dispatchOnPlayHandlers];
@@ -45,7 +50,8 @@ export declare class Reproduction {
45
50
  PAUSED: number;
46
51
  };
47
52
  static newBuilder(): ReproductionBuilder;
48
- on(eventName: keyof typeof Reproduction.EVENTS, handler: Handler): number | undefined;
53
+ on(eventName: keyof typeof Reproduction.EVENTS, handler: Handler): () => void;
54
+ off(eventName: keyof typeof Reproduction.EVENTS, handler: Handler): void;
49
55
  dispatch(eventName: keyof typeof Reproduction.EVENTS, args?: {}): void;
50
56
  start(): void;
51
57
  play(): void;
@@ -56,6 +62,7 @@ export declare class Reproduction {
56
62
  isStopped(): boolean;
57
63
  isPaused(): boolean;
58
64
  isCountingIn(): boolean;
65
+ getState(): ReproductionState;
59
66
  getPlayer(): Player;
60
67
  getTempo(): number;
61
68
  getCurrentTime(): number;
@@ -63,6 +70,7 @@ export declare class Reproduction {
63
70
  seekTo(seconds: number): void;
64
71
  getVolume(): number;
65
72
  setVolume(volume: number): void;
73
+ setLoopRange(from: number, to: number): boolean;
66
74
  getAvailablePlaybackRates(): number[];
67
75
  setPlaybackRate(playbackRate: number): void;
68
76
  isAvailable(): boolean;
@@ -1,6 +1,6 @@
1
1
  import { PLAYER_EVENTS } from './Player/PlayerEvents';
2
2
  import { ReproductionBuilder } from './ReproductionBuilder';
3
- const STATES = {
3
+ export const REPRODUCTION_STATES = {
4
4
  STOPPED: 0,
5
5
  COUNTING_IN: 1,
6
6
  PLAYING: 2,
@@ -15,7 +15,6 @@ const EVENTS = {
15
15
  FINISH: 'FINISH',
16
16
  ERROR: 'ERROR',
17
17
  };
18
- const dispatchOnReadyHandlers = Symbol();
19
18
  const dispatchOnSongStartHandlers = Symbol();
20
19
  const dispatchOnCountingInHandlers = Symbol();
21
20
  const dispatchOnPlayHandlers = Symbol();
@@ -25,7 +24,6 @@ const dispatchOnFinishHandlers = Symbol();
25
24
  const dispatchOnErrorHandlers = Symbol();
26
25
  export class Reproduction {
27
26
  constructor(player, requiresCountingIn, songTempo, volume) {
28
- this[dispatchOnReadyHandlers] = [];
29
27
  this[dispatchOnSongStartHandlers] = [];
30
28
  this[dispatchOnCountingInHandlers] = [];
31
29
  this[dispatchOnPlayHandlers] = [];
@@ -62,29 +60,75 @@ export class Reproduction {
62
60
  return EVENTS;
63
61
  }
64
62
  static get STATES() {
65
- return STATES;
63
+ return REPRODUCTION_STATES;
66
64
  }
67
65
  static newBuilder() {
68
66
  return new ReproductionBuilder();
69
67
  }
70
68
  on(eventName, handler) {
69
+ if (typeof handler !== 'function') {
70
+ throw new Error('Handler must be a function');
71
+ }
71
72
  switch (eventName) {
72
73
  case Reproduction.EVENTS.START:
73
- return this[dispatchOnSongStartHandlers].push(handler);
74
+ this[dispatchOnSongStartHandlers].push(handler);
75
+ break;
74
76
  case Reproduction.EVENTS.COUNTING_IN:
75
- return this[dispatchOnCountingInHandlers].push(handler);
77
+ this[dispatchOnCountingInHandlers].push(handler);
78
+ break;
76
79
  case Reproduction.EVENTS.PLAY:
77
- return this[dispatchOnPlayHandlers].push(handler);
80
+ this[dispatchOnPlayHandlers].push(handler);
81
+ break;
78
82
  case Reproduction.EVENTS.PLAYING:
79
- return this[dispatchOnPlayingHandlers].push(handler);
83
+ this[dispatchOnPlayingHandlers].push(handler);
84
+ break;
80
85
  case Reproduction.EVENTS.PAUSED:
81
- return this[dispatchOnPausedHandlers].push(handler);
86
+ this[dispatchOnPausedHandlers].push(handler);
87
+ break;
82
88
  case Reproduction.EVENTS.FINISH:
83
- return this[dispatchOnFinishHandlers].push(handler);
89
+ this[dispatchOnFinishHandlers].push(handler);
90
+ break;
84
91
  case Reproduction.EVENTS.ERROR:
85
- return this[dispatchOnErrorHandlers].push(handler);
92
+ this[dispatchOnErrorHandlers].push(handler);
93
+ break;
86
94
  default:
95
+ throw new Error(`Unknown event: ${eventName}`);
96
+ }
97
+ return () => this.off(eventName, handler);
98
+ }
99
+ off(eventName, handler) {
100
+ if (typeof handler !== 'function') {
101
+ throw new Error('Handler must be a function');
102
+ }
103
+ let handlers;
104
+ switch (eventName) {
105
+ case Reproduction.EVENTS.START:
106
+ handlers = this[dispatchOnSongStartHandlers];
87
107
  break;
108
+ case Reproduction.EVENTS.COUNTING_IN:
109
+ handlers = this[dispatchOnCountingInHandlers];
110
+ break;
111
+ case Reproduction.EVENTS.PLAY:
112
+ handlers = this[dispatchOnPlayHandlers];
113
+ break;
114
+ case Reproduction.EVENTS.PLAYING:
115
+ handlers = this[dispatchOnPlayingHandlers];
116
+ break;
117
+ case Reproduction.EVENTS.PAUSED:
118
+ handlers = this[dispatchOnPausedHandlers];
119
+ break;
120
+ case Reproduction.EVENTS.FINISH:
121
+ handlers = this[dispatchOnFinishHandlers];
122
+ break;
123
+ case Reproduction.EVENTS.ERROR:
124
+ handlers = this[dispatchOnErrorHandlers];
125
+ break;
126
+ default:
127
+ throw new Error(`Unknown event: ${eventName}`);
128
+ }
129
+ const index = handlers.indexOf(handler);
130
+ if (index > -1) {
131
+ handlers.splice(index, 1);
88
132
  }
89
133
  }
90
134
  dispatch(eventName, args = {}) {
@@ -136,23 +180,20 @@ export class Reproduction {
136
180
  play() {
137
181
  clearInterval(this.interval);
138
182
  this.player.play();
139
- const intervalTimeout = 200;
183
+ const bpmInterval = this.getBPMInterval();
184
+ const tickInterval = Math.min(bpmInterval / 4, 50);
140
185
  this.interval = setInterval(() => {
141
186
  if (this.isPlaying()) {
142
187
  this.dispatch(Reproduction.EVENTS.PLAYING);
143
188
  }
144
- }, intervalTimeout);
189
+ }, tickInterval);
145
190
  }
146
191
  playLoop(from, to) {
147
- if (!Number.isFinite(from) || !Number.isFinite(to)) {
148
- return;
149
- }
150
- if (to <= from) {
192
+ if (!this.setLoopRange(from, to)) {
151
193
  return;
152
194
  }
153
195
  clearInterval(this.loopInterval);
154
196
  this.loopInterval = null;
155
- this.loopRange = { from, to };
156
197
  this.seekTo(from);
157
198
  this.play();
158
199
  const loopCheckInterval = 100;
@@ -196,6 +237,9 @@ export class Reproduction {
196
237
  isCountingIn() {
197
238
  return this.state === Reproduction.STATES.COUNTING_IN;
198
239
  }
240
+ getState() {
241
+ return this.state;
242
+ }
199
243
  getPlayer() {
200
244
  return this.player;
201
245
  }
@@ -224,6 +268,13 @@ export class Reproduction {
224
268
  }
225
269
  this.player.setVolume(volume);
226
270
  }
271
+ setLoopRange(from, to) {
272
+ if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) {
273
+ return false;
274
+ }
275
+ this.loopRange = { from, to };
276
+ return true;
277
+ }
227
278
  getAvailablePlaybackRates() {
228
279
  return this.player.getAvailablePlaybackRates();
229
280
  }
@@ -239,7 +290,9 @@ export class Reproduction {
239
290
  countInAndPlay(timeout, limit) {
240
291
  // the initial count starts instantly, no need to wait
241
292
  this.countingInCounter++;
242
- this.dispatch(Reproduction.EVENTS.COUNTING_IN, { countingInCounter: this.countingInCounter });
293
+ this.dispatch(Reproduction.EVENTS.COUNTING_IN, {
294
+ countingInCounter: this.countingInCounter,
295
+ });
243
296
  const interval = setInterval(() => {
244
297
  this.countingInCounter++;
245
298
  if (this.countingInCounter === limit) {
@@ -253,7 +306,9 @@ export class Reproduction {
253
306
  }
254
307
  }
255
308
  else {
256
- this.dispatch(Reproduction.EVENTS.COUNTING_IN, { countingInCounter: this.countingInCounter });
309
+ this.dispatch(Reproduction.EVENTS.COUNTING_IN, {
310
+ countingInCounter: this.countingInCounter,
311
+ });
257
312
  }
258
313
  }, timeout);
259
314
  }
@@ -45,7 +45,7 @@ const RangeSelectorCanvas = ({ selectedRange, onChange, onRangeChange, }) => {
45
45
  context.fillRect(pixelX0, 0, pixelX1 - pixelX0, context.canvas.height);
46
46
  context.globalAlpha = 1.0;
47
47
  };
48
- // Check if mouse is near the start or end edge of the selection
48
+ // Check if the mouse pointer is near the start or end edge of the selection
49
49
  const isNearSelectionEdge = (pixel) => {
50
50
  if (selectedRange.length !== 2)
51
51
  return { isNear: false, edge: null };
@@ -180,4 +180,10 @@ const RangeSelectorCanvas = ({ selectedRange, onChange, onRangeChange, }) => {
180
180
  }, [selection, zoomContextValue]);
181
181
  return (React.createElement(OverlayCanvas, { ref: canvasRef, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, style: { cursor: cursorStyle }, className: 'media-timeline-range-selector-canvas' }));
182
182
  };
183
- export default React.memo(RangeSelectorCanvas);
183
+ const areEqual = (prevProps, nextProps) => {
184
+ return (prevProps.selectedRange[0] === nextProps.selectedRange[0] &&
185
+ prevProps.selectedRange[1] === nextProps.selectedRange[1] &&
186
+ prevProps.onChange === nextProps.onChange &&
187
+ prevProps.onRangeChange === nextProps.onRangeChange);
188
+ };
189
+ export default React.memo(RangeSelectorCanvas, areEqual);
@@ -9,3 +9,4 @@ export declare const WithCustomClassName: import("storybook/internal/csf").Annot
9
9
  export declare const WithoutTimeBlocks: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
10
10
  export declare const Minimalist: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
11
11
  export declare const WithSelectedRangeAndMarkers: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
12
+ export declare const MediaPlaybackSimulation: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
  import { Timeline } from '../components/timeline';
4
4
  import './timeline.stories.custom.css';
@@ -17,6 +17,65 @@ export default {
17
17
  },
18
18
  };
19
19
  const Template = (args) => (React.createElement(StyledTimeline, Object.assign({}, args)));
20
+ const PlaybackSimulation = (args) => {
21
+ const [currentValue, setCurrentValue] = useState(args.value || 0);
22
+ const [selectedRange, setSelectedRange] = useState(null);
23
+ const [isPlaying, setIsPlaying] = useState(false);
24
+ const intervalRef = useRef(null);
25
+ useEffect(() => {
26
+ if (isPlaying && currentValue < args.duration) {
27
+ intervalRef.current = setInterval(() => {
28
+ setCurrentValue((prev) => {
29
+ const next = prev + 0.05; // Advance 50ms (0.05 seconds)
30
+ return next >= args.duration ? args.duration : next;
31
+ });
32
+ }, 50); // Update every 50ms
33
+ }
34
+ else {
35
+ if (intervalRef.current) {
36
+ clearInterval(intervalRef.current);
37
+ intervalRef.current = null;
38
+ }
39
+ }
40
+ return () => {
41
+ if (intervalRef.current) {
42
+ clearInterval(intervalRef.current);
43
+ }
44
+ };
45
+ }, [isPlaying, currentValue, args.duration]);
46
+ const handlePlayPause = () => {
47
+ if (currentValue >= args.duration) {
48
+ setCurrentValue(0); // Reset to beginning if we reached the end
49
+ }
50
+ setIsPlaying(!isPlaying);
51
+ };
52
+ const handleReset = () => {
53
+ setIsPlaying(false);
54
+ setCurrentValue(0);
55
+ };
56
+ const onRangeChangeHandler = (values) => {
57
+ setSelectedRange(values);
58
+ };
59
+ return (React.createElement("div", null,
60
+ React.createElement("div", { style: { marginBottom: '10px' } },
61
+ React.createElement("button", { onClick: handlePlayPause, style: { marginRight: '10px' } }, isPlaying ? 'Pause' : 'Play'),
62
+ React.createElement("button", { onClick: handleReset }, "Reset"),
63
+ React.createElement("span", { style: { marginLeft: '20px' } },
64
+ "Time: ",
65
+ currentValue.toFixed(2),
66
+ "s / ",
67
+ args.duration,
68
+ "s"),
69
+ React.createElement("span", { style: { marginLeft: '20px' } },
70
+ "Selected range:",
71
+ ' ',
72
+ selectedRange ? `${selectedRange[0]}-${selectedRange[1]}` : 'N/A')),
73
+ React.createElement(StyledTimeline, Object.assign({}, args, { value: currentValue, onChange: (value) => {
74
+ var _a;
75
+ setCurrentValue(value);
76
+ (_a = args.onChange) === null || _a === void 0 ? void 0 : _a.call(args, value);
77
+ }, onRangeChange: onRangeChangeHandler }))));
78
+ };
20
79
  export const Default = Template.bind({});
21
80
  Default.args = {
22
81
  duration: 305,
@@ -61,3 +120,11 @@ WithSelectedRangeAndMarkers.args = {
61
120
  selectedRange: [20, 30],
62
121
  markers: [90, 108],
63
122
  };
123
+ export const MediaPlaybackSimulation = PlaybackSimulation.bind({});
124
+ MediaPlaybackSimulation.args = {
125
+ duration: 120, // 2 minutes
126
+ value: 0,
127
+ zoomLevel: 0,
128
+ selectedRange: [30, 45], // Show a selected range
129
+ markers: [15, 60, 90], // Add some markers
130
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldelia/react-media",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "A React components collection for media-related features.",
5
5
  "private": false,
6
6
  "keywords": [
@@ -114,11 +114,11 @@
114
114
  "@babel/preset-react": "^7.24.7",
115
115
  "@babel/preset-typescript": "^7.24.7",
116
116
  "@chromatic-com/storybook": "^5.0.1",
117
- "@storybook/addon-docs": "^10.2.8",
118
- "@storybook/addon-links": "^10.2.8",
119
- "@storybook/addon-onboarding": "^10.2.8",
117
+ "@storybook/addon-docs": "^10.2.13",
118
+ "@storybook/addon-links": "^10.2.13",
119
+ "@storybook/addon-onboarding": "^10.2.13",
120
120
  "@storybook/react-docgen-typescript-plugin": "^1.0.6--canary.9.0c3f3b7.0",
121
- "@storybook/react-webpack5": "^10.2.8",
121
+ "@storybook/react-webpack5": "^10.2.13",
122
122
  "@testing-library/jest-dom": "^6.4.6",
123
123
  "@testing-library/react": "^16.0.0",
124
124
  "@testing-library/user-event": "^14.5.2",
@@ -131,7 +131,7 @@
131
131
  "babel-loader": "^9.1.3",
132
132
  "eslint-config-prettier": "^9.1.0",
133
133
  "eslint-plugin-prettier": "^5.1.3",
134
- "eslint-plugin-storybook": "^10.2.8",
134
+ "eslint-plugin-storybook": "^10.2.13",
135
135
  "jest-environment-jsdom": "^30.2.0",
136
136
  "postcss-flexbugs-fixes": "^5.0.2",
137
137
  "postcss-normalize": "^10.0.1",
@@ -139,7 +139,7 @@
139
139
  "prettier": "^3.3.2",
140
140
  "prop-types": "^15.8.1",
141
141
  "react-docgen-typescript-plugin": "^1.0.8",
142
- "storybook": "^10.2.8",
142
+ "storybook": "^10.2.13",
143
143
  "ts-loader": "^9.5.1",
144
144
  "tsconfig-paths-webpack-plugin": "^4.1.0",
145
145
  "webpack": "^5.105.2"