@ldelia/react-media 0.8.0 → 0.8.2

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.
@@ -13,12 +13,12 @@ export declare class PlayAlongPlayer {
13
13
  static get EVENTS(): {
14
14
  readonly READY: "READY";
15
15
  readonly FINISH: "FINISH";
16
+ readonly ERROR: "ERROR";
16
17
  };
17
18
  play(): void;
18
19
  pause(): void;
19
20
  stop(): void;
20
21
  seekTo(seconds: number): void;
21
- setVolume(volume: number): void;
22
22
  getCurrentTime(): number;
23
23
  getDuration(): number;
24
24
  isAvailable(): boolean;
@@ -26,6 +26,10 @@ export declare class PlayAlongPlayer {
26
26
  setPlaybackRate(playbackRate: number): void;
27
27
  on(eventName: keyof typeof PlayAlongPlayer.EVENTS, handler: () => void): number | undefined;
28
28
  dispatch(eventName: keyof typeof PlayAlongPlayer.EVENTS): void;
29
+ countingStarted(): void;
30
+ countingFinished(): void;
31
+ setVolume(volume: number): void;
32
+ getVolume(): number;
29
33
  private setInnerPlayer;
30
34
  }
31
35
  export {};
@@ -44,7 +44,6 @@ export class PlayAlongPlayer {
44
44
  seekTo(seconds) {
45
45
  this.currentTime = seconds;
46
46
  }
47
- setVolume(volume) { }
48
47
  getCurrentTime() {
49
48
  return this.currentTime;
50
49
  }
@@ -65,8 +64,6 @@ export class PlayAlongPlayer {
65
64
  }
66
65
  on(eventName, handler) {
67
66
  switch (eventName) {
68
- case PlayAlongPlayer.EVENTS.READY:
69
- return this[dispatchOnReadyHandlers].push(handler);
70
67
  case PlayAlongPlayer.EVENTS.FINISH:
71
68
  return this[dispatchOnFinishHandlers].push(handler);
72
69
  default:
@@ -77,9 +74,6 @@ export class PlayAlongPlayer {
77
74
  let handler, i, len;
78
75
  let ref = [];
79
76
  switch (eventName) {
80
- case PlayAlongPlayer.EVENTS.READY:
81
- ref = this[dispatchOnReadyHandlers];
82
- break;
83
77
  case PlayAlongPlayer.EVENTS.FINISH:
84
78
  ref = this[dispatchOnFinishHandlers];
85
79
  break;
@@ -91,8 +85,11 @@ export class PlayAlongPlayer {
91
85
  handler();
92
86
  }
93
87
  }
88
+ countingStarted() { }
89
+ countingFinished() { }
90
+ setVolume(volume) { }
91
+ getVolume() { return 0; }
94
92
  setInnerPlayer(innerPlayer) {
95
93
  this.innerPlayer = innerPlayer;
96
- this.dispatch(PlayAlongPlayer.EVENTS.READY);
97
94
  }
98
95
  }
@@ -1,4 +1,5 @@
1
1
  export declare const PLAYER_EVENTS: {
2
2
  readonly READY: "READY";
3
3
  readonly FINISH: "FINISH";
4
+ readonly ERROR: "ERROR";
4
5
  };
@@ -1,4 +1,5 @@
1
1
  export const PLAYER_EVENTS = {
2
2
  READY: 'READY',
3
3
  FINISH: 'FINISH',
4
+ ERROR: 'ERROR',
4
5
  };
@@ -3,16 +3,20 @@ import { PlayAlongPlayer } from './PlayAlongPlayer';
3
3
  export type InnerYouTubePlayerInterface = YT.Player;
4
4
  declare const dispatchOnReadyHandlers: unique symbol;
5
5
  declare const dispatchOnFinishHandlers: unique symbol;
6
+ declare const dispatchOnErrorHandlers: unique symbol;
6
7
  export declare class YouTubePlayer {
7
8
  private currentTime;
8
9
  private isRunning;
10
+ private volume;
9
11
  private innerPlayer;
10
12
  private [dispatchOnReadyHandlers];
11
13
  private [dispatchOnFinishHandlers];
14
+ private [dispatchOnErrorHandlers];
12
15
  constructor(innerPlayer: InnerYouTubePlayerInterface);
13
16
  static get EVENTS(): {
14
17
  readonly READY: "READY";
15
18
  readonly FINISH: "FINISH";
19
+ readonly ERROR: "ERROR";
16
20
  };
17
21
  getInnerPlayer(): YT.Player;
18
22
  play(): void;
@@ -20,12 +24,16 @@ export declare class YouTubePlayer {
20
24
  stop(): void;
21
25
  seekTo(seconds: number): void;
22
26
  setVolume(volume: number): void;
27
+ getVolume(): number;
23
28
  getCurrentTime(): number;
24
29
  getDuration(): number | undefined;
25
30
  getAvailablePlaybackRates(): number[];
26
31
  setPlaybackRate(playbackRate: number): void;
27
32
  isAvailable(): boolean;
28
- on(eventName: keyof typeof PlayAlongPlayer.EVENTS, handler: () => void): number | undefined;
29
- dispatch(eventName: keyof typeof PlayAlongPlayer.EVENTS): void;
33
+ on(eventName: keyof typeof PlayAlongPlayer.EVENTS, handler: (error?: any) => void): number | undefined;
34
+ dispatch(eventName: keyof typeof PlayAlongPlayer.EVENTS, error?: any): void;
35
+ countingStarted(): void;
36
+ countingFinished(): void;
37
+ private getErrorMessage;
30
38
  }
31
39
  export {};
@@ -2,19 +2,27 @@ import { PlayAlongPlayer } from './PlayAlongPlayer';
2
2
  import { PLAYER_EVENTS } from './PlayerEvents';
3
3
  const dispatchOnReadyHandlers = Symbol();
4
4
  const dispatchOnFinishHandlers = Symbol();
5
+ const dispatchOnErrorHandlers = Symbol();
5
6
  export class YouTubePlayer {
6
7
  constructor(innerPlayer) {
8
+ this.volume = 50; // between 0 and 100
7
9
  this[dispatchOnFinishHandlers] = [];
8
10
  this[dispatchOnReadyHandlers] = [];
11
+ this[dispatchOnErrorHandlers] = [];
9
12
  this.currentTime = 0;
10
13
  this.isRunning = false;
11
14
  this.innerPlayer = innerPlayer;
12
- this.innerPlayer = innerPlayer;
13
- this.dispatch(YouTubePlayer.EVENTS.READY);
14
15
  // This is necessary for avoiding the state video cued.
15
16
  // When a video is in this state, when the user seeks to X, the song is played
16
17
  this.innerPlayer.playVideo();
17
18
  this.innerPlayer.pauseVideo();
19
+ this.innerPlayer.addEventListener('onError', (event) => {
20
+ this.isRunning = false;
21
+ this.dispatch(YouTubePlayer.EVENTS.ERROR, {
22
+ code: event.data,
23
+ message: this.getErrorMessage(event.data)
24
+ });
25
+ });
18
26
  this.innerPlayer.addEventListener('onStateChange', (videoState) => {
19
27
  switch (videoState.data) {
20
28
  case YT.PlayerState.ENDED:
@@ -62,8 +70,12 @@ export class YouTubePlayer {
62
70
  }
63
71
  }
64
72
  setVolume(volume) {
73
+ this.volume = volume;
65
74
  this.getInnerPlayer().setVolume(volume);
66
75
  }
76
+ getVolume() {
77
+ return this.volume;
78
+ }
67
79
  getCurrentTime() {
68
80
  return this.isRunning
69
81
  ? this.getInnerPlayer().getCurrentTime()
@@ -88,30 +100,72 @@ export class YouTubePlayer {
88
100
  }
89
101
  on(eventName, handler) {
90
102
  switch (eventName) {
91
- case PlayAlongPlayer.EVENTS.READY:
92
- return this[dispatchOnReadyHandlers].push(handler);
93
103
  case PlayAlongPlayer.EVENTS.FINISH:
94
104
  return this[dispatchOnFinishHandlers].push(handler);
105
+ case PlayAlongPlayer.EVENTS.ERROR:
106
+ return this[dispatchOnErrorHandlers].push(handler);
95
107
  default:
96
108
  break;
97
109
  }
98
110
  }
99
- dispatch(eventName) {
100
- let handler, i, len;
111
+ dispatch(eventName, error) {
112
+ let handler;
113
+ let i;
114
+ let len;
101
115
  let ref = [];
102
116
  switch (eventName) {
103
- case YouTubePlayer.EVENTS.READY:
104
- ref = this[dispatchOnReadyHandlers];
105
- break;
106
117
  case YouTubePlayer.EVENTS.FINISH:
107
118
  ref = this[dispatchOnFinishHandlers];
108
119
  break;
120
+ case YouTubePlayer.EVENTS.ERROR:
121
+ ref = this[dispatchOnErrorHandlers];
122
+ break;
109
123
  default:
110
124
  break;
111
125
  }
112
126
  for (i = 0, len = ref.length; i < len; i++) {
113
127
  handler = ref[i];
114
- setTimeout(handler, 0);
128
+ setTimeout(() => handler(error), 0);
129
+ }
130
+ }
131
+ countingStarted() {
132
+ /**
133
+ * iOS browsers enforce strict autoplay policies that require video playback
134
+ * to begin synchronously within a user interaction event (e.g., a tap or click).
135
+ * When we implemented the counting-in feature (1, 2, 1, 2, 3, go...),
136
+ * the asynchronous delays between counts caused iOS to lose the user interaction context,
137
+ * blocking video playback after the countdown completed.
138
+ * The video would work fine on web and Android, but fail to start on iOS Safari.
139
+ * To resolve this, we adopted a muted-first approach:
140
+ * the video begins playing immediately when the user initiates playback,
141
+ * but remains muted during the countdown sequence.
142
+ * Once the countdown completes, we unmute the video and restore the desired volume.
143
+ * This strategy satisfies iOS's autoplay requirements (muted videos can autoplay)
144
+ * while preserving the musical countdown experience.
145
+ * The user never notices the video is playing during the countdown
146
+ * since it's both muted and visually obscured by the countdown overlay.
147
+ * */
148
+ this.getInnerPlayer().setVolume(0);
149
+ this.getInnerPlayer().playVideo();
150
+ }
151
+ countingFinished() {
152
+ this.getInnerPlayer().pauseVideo();
153
+ this.getInnerPlayer().seekTo(0, true);
154
+ this.setVolume(this.volume);
155
+ }
156
+ getErrorMessage(errorCode) {
157
+ switch (errorCode) {
158
+ case YT.PlayerError.InvalidParam:
159
+ return 'Invalid parameter value';
160
+ case YT.PlayerError.Html5Error:
161
+ return 'HTML5 player error';
162
+ case YT.PlayerError.VideoNotFound:
163
+ return 'Video not found';
164
+ case YT.PlayerError.EmbeddingNotAllowed:
165
+ case YT.PlayerError.EmbeddingNotAllowed2:
166
+ return 'Video cannot be played in embedded players';
167
+ default:
168
+ return `Unknown error (code: ${errorCode})`;
115
169
  }
116
170
  }
117
171
  }
@@ -10,15 +10,14 @@ declare const dispatchOnPlayHandlers: unique symbol;
10
10
  declare const dispatchOnPlayingHandlers: unique symbol;
11
11
  declare const dispatchOnPausedHandlers: unique symbol;
12
12
  declare const dispatchOnFinishHandlers: unique symbol;
13
+ declare const dispatchOnErrorHandlers: unique symbol;
13
14
  export declare class Reproduction {
14
15
  private player;
15
16
  private requiresCountingIn;
16
17
  private songTempo;
17
18
  private state;
18
- private ready;
19
19
  private interval;
20
20
  private countingInCounter;
21
- private volume;
22
21
  private [dispatchOnReadyHandlers];
23
22
  private [dispatchOnSongStartHandlers];
24
23
  private [dispatchOnCountingInHandlers];
@@ -26,15 +25,16 @@ export declare class Reproduction {
26
25
  private [dispatchOnPlayingHandlers];
27
26
  private [dispatchOnPausedHandlers];
28
27
  private [dispatchOnFinishHandlers];
28
+ private [dispatchOnErrorHandlers];
29
29
  constructor(player: Player, requiresCountingIn: boolean, songTempo: number, volume: number);
30
30
  static get EVENTS(): {
31
- readonly READY: "READY";
32
31
  readonly START: "START";
33
32
  readonly COUNTING_IN: "COUNTING_IN";
34
33
  readonly PLAY: "PLAY";
35
34
  readonly PLAYING: "PLAYING";
36
35
  readonly PAUSED: "PAUSED";
37
36
  readonly FINISH: "FINISH";
37
+ readonly ERROR: "ERROR";
38
38
  };
39
39
  static get STATES(): {
40
40
  STOPPED: number;
@@ -49,7 +49,6 @@ export declare class Reproduction {
49
49
  play(): void;
50
50
  pause(): void;
51
51
  stop(): void;
52
- isReady(): boolean;
53
52
  isPlaying(): boolean;
54
53
  isStopped(): boolean;
55
54
  isPaused(): boolean;
@@ -7,13 +7,13 @@ const STATES = {
7
7
  PAUSED: 3,
8
8
  };
9
9
  const EVENTS = {
10
- READY: 'READY',
11
10
  START: 'START',
12
11
  COUNTING_IN: 'COUNTING_IN',
13
12
  PLAY: 'PLAY',
14
13
  PLAYING: 'PLAYING',
15
14
  PAUSED: 'PAUSED',
16
15
  FINISH: 'FINISH',
16
+ ERROR: 'ERROR',
17
17
  };
18
18
  const dispatchOnReadyHandlers = Symbol();
19
19
  const dispatchOnSongStartHandlers = Symbol();
@@ -22,9 +22,9 @@ const dispatchOnPlayHandlers = Symbol();
22
22
  const dispatchOnPlayingHandlers = Symbol();
23
23
  const dispatchOnPausedHandlers = Symbol();
24
24
  const dispatchOnFinishHandlers = Symbol();
25
+ const dispatchOnErrorHandlers = Symbol();
25
26
  export class Reproduction {
26
27
  constructor(player, requiresCountingIn, songTempo, volume) {
27
- this.volume = 50; // between 0 and 100
28
28
  this[dispatchOnReadyHandlers] = [];
29
29
  this[dispatchOnSongStartHandlers] = [];
30
30
  this[dispatchOnCountingInHandlers] = [];
@@ -32,23 +32,22 @@ export class Reproduction {
32
32
  this[dispatchOnPlayingHandlers] = [];
33
33
  this[dispatchOnPausedHandlers] = [];
34
34
  this[dispatchOnFinishHandlers] = [];
35
+ this[dispatchOnErrorHandlers] = [];
35
36
  this.songTempo = songTempo;
36
37
  this.player = player;
37
- this.ready = false;
38
38
  this.state = Reproduction.STATES.STOPPED;
39
39
  this.interval = null;
40
40
  this.requiresCountingIn = requiresCountingIn;
41
41
  this.countingInCounter = 0;
42
- this.setVolume(volume);
43
- this.player.on(PLAYER_EVENTS.READY, () => {
44
- this.ready = true;
45
- this.dispatch(Reproduction.EVENTS.READY);
46
- });
42
+ this.player.setVolume(volume);
47
43
  this.player.on(PLAYER_EVENTS.FINISH, () => {
48
44
  this.state = Reproduction.STATES.STOPPED;
49
45
  clearInterval(this.interval);
50
46
  this.dispatch(Reproduction.EVENTS.FINISH);
51
47
  });
48
+ this.player.on(PLAYER_EVENTS.ERROR, (error) => {
49
+ this.dispatch(Reproduction.EVENTS.ERROR, { error });
50
+ });
52
51
  }
53
52
  static get EVENTS() {
54
53
  return EVENTS;
@@ -61,8 +60,6 @@ export class Reproduction {
61
60
  }
62
61
  on(eventName, handler) {
63
62
  switch (eventName) {
64
- case Reproduction.EVENTS.READY:
65
- return this[dispatchOnReadyHandlers].push(handler);
66
63
  case Reproduction.EVENTS.START:
67
64
  return this[dispatchOnSongStartHandlers].push(handler);
68
65
  case Reproduction.EVENTS.COUNTING_IN:
@@ -75,6 +72,8 @@ export class Reproduction {
75
72
  return this[dispatchOnPausedHandlers].push(handler);
76
73
  case Reproduction.EVENTS.FINISH:
77
74
  return this[dispatchOnFinishHandlers].push(handler);
75
+ case Reproduction.EVENTS.ERROR:
76
+ return this[dispatchOnErrorHandlers].push(handler);
78
77
  default:
79
78
  break;
80
79
  }
@@ -83,9 +82,6 @@ export class Reproduction {
83
82
  let handler, i, len;
84
83
  let ref = [];
85
84
  switch (eventName) {
86
- case Reproduction.EVENTS.READY:
87
- ref = this[dispatchOnReadyHandlers];
88
- break;
89
85
  case Reproduction.EVENTS.START:
90
86
  ref = this[dispatchOnSongStartHandlers];
91
87
  break;
@@ -104,13 +100,15 @@ export class Reproduction {
104
100
  case Reproduction.EVENTS.FINISH:
105
101
  ref = this[dispatchOnFinishHandlers];
106
102
  break;
103
+ case Reproduction.EVENTS.ERROR:
104
+ ref = this[dispatchOnErrorHandlers];
105
+ break;
107
106
  default:
108
107
  break;
109
108
  }
110
109
  for (i = 0, len = ref.length; i < len; i++) {
111
110
  handler = ref[i];
112
111
  handler(args);
113
- // setTimeout(handler, 0);
114
112
  }
115
113
  }
116
114
  start() {
@@ -148,10 +146,6 @@ export class Reproduction {
148
146
  clearInterval(this.interval);
149
147
  this.dispatch(Reproduction.EVENTS.FINISH);
150
148
  }
151
- isReady() {
152
- // It's necessary to avoid play the reproduction-widget when the player is not ready
153
- return this.ready;
154
- }
155
149
  isPlaying() {
156
150
  return this.state === Reproduction.STATES.PLAYING;
157
151
  }
@@ -181,7 +175,7 @@ export class Reproduction {
181
175
  this.player.seekTo(seconds);
182
176
  }
183
177
  getVolume() {
184
- return this.volume;
178
+ return this.player.getVolume();
185
179
  }
186
180
  setVolume(volume) {
187
181
  if (volume < 0) {
@@ -190,7 +184,6 @@ export class Reproduction {
190
184
  else if (volume > 100) {
191
185
  volume = 100;
192
186
  }
193
- this.volume = volume;
194
187
  this.player.setVolume(volume);
195
188
  }
196
189
  getAvailablePlaybackRates() {
@@ -207,6 +200,7 @@ export class Reproduction {
207
200
  }
208
201
  countInAndPlay(timeout, limit) {
209
202
  // the initial count starts instantly, no need to wait
203
+ this.player.countingStarted();
210
204
  this.countingInCounter++;
211
205
  this.dispatch(Reproduction.EVENTS.COUNTING_IN, { countingInCounter: this.countingInCounter });
212
206
  const interval = setInterval(() => {
@@ -218,6 +212,7 @@ export class Reproduction {
218
212
  this.countInAndPlay(this.getBPMInterval(), 5);
219
213
  }
220
214
  else {
215
+ this.player.countingFinished();
221
216
  this.play();
222
217
  }
223
218
  }
@@ -18,7 +18,7 @@ const Template = (args) => {
18
18
  reproductionInstance.on('PLAYING', refreshEvent);
19
19
  reproductionInstance.on('PAUSED', refreshEvent);
20
20
  reproductionInstance.on('FINISH', refreshEvent);
21
- reproductionInstance.start();
21
+ reproductionInstance.on('ERROR', (args) => { console.error("Reproduction error", args); });
22
22
  }, []);
23
23
  const handleStop = () => {
24
24
  if (reproduction) {
@@ -35,12 +35,18 @@ const Template = (args) => {
35
35
  reproduction.play();
36
36
  }
37
37
  };
38
+ const handleStart = () => {
39
+ if (reproduction) {
40
+ reproduction.start();
41
+ }
42
+ };
38
43
  return (React.createElement("div", null,
39
44
  React.createElement(ReproductionWidget, Object.assign({}, args, { onInit: handleInit })),
40
45
  React.createElement("div", null,
41
46
  React.createElement("button", { onClick: handleStop, disabled: !reproduction || reproduction.isStopped() }, "Stop"),
42
47
  React.createElement("button", { onClick: handlePause, disabled: !reproduction || !reproduction.isPlaying() }, "Pause"),
43
- React.createElement("button", { onClick: handleResume, disabled: !reproduction || reproduction.isPlaying() }, "Resume"),
48
+ React.createElement("button", { onClick: handleResume, disabled: !reproduction || reproduction.isPlaying() || reproduction.getCurrentTime() > 0 }, "Resume"),
49
+ React.createElement("button", { onClick: handleStart, disabled: !reproduction || reproduction.isPlaying() }, "Start"),
44
50
  reproduction && (React.createElement("div", null,
45
51
  "Current time: ", reproduction === null || reproduction === void 0 ? void 0 :
46
52
  reproduction.getCurrentTime())),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldelia/react-media",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "A React components collection for media-related features.",
5
5
  "private": false,
6
6
  "keywords": [
@@ -127,7 +127,7 @@
127
127
  "@types/react": "^18.3.3",
128
128
  "@types/react-dom": "^18.3.0",
129
129
  "@types/styled-components": "^5.1.34",
130
- "@types/youtube": "^0.0.50",
130
+ "@types/youtube": "^0.1.2",
131
131
  "babel-loader": "^9.1.3",
132
132
  "eslint-config-prettier": "^9.1.0",
133
133
  "eslint-plugin-prettier": "^5.1.3",