diff --git a/.editorconfig b/.editorconfig
index d3fbd2050..7f886dfde 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,6 +1,6 @@
root = true
-[{src,scripts}/**.{ts,json,js}]
+[{src,web,scripts}/**.{ts,json,js}]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
diff --git a/README.md b/README.md
index 74966566d..0e34c7f6e 100644
--- a/README.md
+++ b/README.md
@@ -273,3 +273,14 @@ You can find us as part of the [React Native Track Player](https://discordapp.co
- `# support` - Ask members of the community to trouble shoot issues with your app and make recommendations.
- `# app-anouncements` - Tell the community about the app you made with this project!
- `# releases` - Stay updated about the latest releases and dev efforts on the project.
+
+
+### Web Notes
+
+https://github.com/shaka-project/shaka-player/blob/7772099029acb47e6905a688f6cfc9c8738c6ff2/docs/tutorials/faq.md
+
+Q: Why doesn't my HLS content work?
+
+A: If your HLS content uses MPEG2-TS, you may need to enable transmuxing. The only browsers capable of playing TS natively are Edge and Chromecast. You will get a CONTENT_UNSUPPORTED_BY_BROWSER error on other browsers due to their lack of TS support.
+
+You can enable transmuxing by including mux.js v5.6.3+ in your application. If Shaka Player detects that mux.js has been loaded, we will use it to transmux TS content into MP4 on-the-fly, so that the content can be played by the browser.
diff --git a/example/package.json b/example/package.json
index 3db66eeef..8383129fe 100644
--- a/example/package.json
+++ b/example/package.json
@@ -21,6 +21,7 @@
},
"dependencies": {
"@react-native-community/slider": "^4.4.0",
+ "mux.js": "^6.2.0",
"react": "17.0.2",
"react-dom": "^17.0.2",
"react-native": "0.68.1",
diff --git a/example/public/icon.png b/example/public/icon.png
new file mode 100644
index 000000000..151c252b2
Binary files /dev/null and b/example/public/icon.png differ
diff --git a/example/public/index.html b/example/public/index.html
index 8b10b9fe8..0d0e22d71 100644
--- a/example/public/index.html
+++ b/example/public/index.html
@@ -6,7 +6,8 @@
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
-
Your App Title
+
+ RNTP Example App
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 2028855f1..d5b06fe1b 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -5,6 +5,7 @@ import {
StatusBar,
StyleSheet,
View,
+ Platform,
} from 'react-native';
import TrackPlayer, { useActiveTrack } from 'react-native-track-player';
@@ -67,6 +68,7 @@ const styles = StyleSheet.create({
backgroundColor: '#212121',
alignItems: 'center',
justifyContent: 'center',
+ minHeight: Platform.OS === 'web' ? '100vh' : '100%',
},
contentContainer: {
flex: 3,
diff --git a/example/src/components/Progress.tsx b/example/src/components/Progress.tsx
index 4175570bf..c5c739812 100644
--- a/example/src/components/Progress.tsx
+++ b/example/src/components/Progress.tsx
@@ -10,7 +10,7 @@ export const Progress: React.FC<{ live?: boolean }> = ({ live }) => {
Live Stream
) : (
- <>
+ = ({ live }) => {
{formatSeconds(Math.max(0, duration - position))}
- >
+
);
};
diff --git a/example/src/index.ts b/example/src/index.ts
index 0db8bf4af..106350158 100644
--- a/example/src/index.ts
+++ b/example/src/index.ts
@@ -3,6 +3,9 @@
************************************************/
import {AppRegistry} from 'react-native';
import App from './App';
+import TrackPlayer from 'react-native-track-player';
+import {PlaybackService} from './services';
+import 'mux.js';
const appName = 'Your app name';
@@ -11,3 +14,5 @@ AppRegistry.runApplication(appName, {
// Mount the react-native app in the 'root' div of index.html
rootTag: document.getElementById('root'),
});
+
+TrackPlayer.registerPlaybackService(() => PlaybackService);
diff --git a/example/src/services/PlaybackService.ts b/example/src/services/PlaybackService.ts
index 66e88eef0..7085c4762 100644
--- a/example/src/services/PlaybackService.ts
+++ b/example/src/services/PlaybackService.ts
@@ -48,6 +48,11 @@ export async function PlaybackService() {
console.log('Event.PlaybackActiveTrackChanged', event);
});
+ TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, (event) => {
+ console.log('Event.PlaybackProgressUpdated', event);
+ });
+
+
TrackPlayer.addEventListener(Event.PlaybackPlayWhenReadyChanged, (event) => {
console.log('Event.PlaybackPlayWhenReadyChanged', event);
});
diff --git a/example/yarn.lock b/example/yarn.lock
index 6fd3907c1..b093b1d71 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -5071,6 +5071,11 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0"
entities "^2.0.0"
+dom-walk@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
+ integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
+
domelementtype@1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
@@ -6385,6 +6390,14 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
+global@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
+ integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
+ dependencies:
+ min-document "^2.19.0"
+ process "^0.11.10"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -9268,6 +9281,13 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==
+ dependencies:
+ dom-walk "^0.1.0"
+
mini-css-extract-plugin@^2.4.5:
version "2.7.2"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz#e049d3ea7d3e4e773aad585c6cb329ce0c7b72d7"
@@ -9342,6 +9362,14 @@ mustache@^4.0.1:
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+mux.js@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-6.2.0.tgz#158a4fcf5d83b087ab9037d325527ea993f830a3"
+ integrity sha512-SKuxIcbmK/aJoz78aQNuoXY8R/uEPm1gQMqWTXL6DNl7oF8UPjdt/AunXGkPQpBouGWKDgL/TzSl2VV5NuboRg==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ global "^4.4.0"
+
nan@^2.12.1:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
@@ -10647,6 +10675,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
diff --git a/package.json b/package.json
index b39824635..535e94895 100644
--- a/package.json
+++ b/package.json
@@ -2,8 +2,8 @@
"name": "react-native-track-player",
"version": "3.2.0",
"description": "A fully fledged audio module created for music apps",
- "main": "lib/index.js",
- "types": "lib/index.d.ts",
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.ts",
"react-native": "src/index",
"source": "src/index",
"scripts": {
@@ -31,6 +31,7 @@
"lib/**/*",
"ios/**/*",
"android/**/*",
+ "web/**/*",
"*.podspec"
],
"contributors": [
@@ -93,15 +94,18 @@
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.0",
"prettier": "^2.7.1",
- "rimraf": "^2.6.2",
"react": "17.0.2",
"react-native": "0.68.1",
"react-native-windows": "^0.65.0-0",
+ "rimraf": "^2.6.2",
"typescript": "^4.8.2"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
+ },
+ "dependencies": {
+ "shaka-player": "^4.3.2"
}
}
diff --git a/src/TrackPlayerModule.ts b/src/TrackPlayerModule.ts
new file mode 100644
index 000000000..ed5c627bb
--- /dev/null
+++ b/src/TrackPlayerModule.ts
@@ -0,0 +1,3 @@
+import { NativeModules } from 'react-native'
+const { TrackPlayerModule } = NativeModules
+export default TrackPlayerModule;
diff --git a/src/TrackPlayerModule.web.ts b/src/TrackPlayerModule.web.ts
new file mode 100644
index 000000000..0fc943e28
--- /dev/null
+++ b/src/TrackPlayerModule.web.ts
@@ -0,0 +1,2 @@
+import TrackPlayerModule from '../web';
+export default TrackPlayerModule;
diff --git a/src/constants/Capability.ts b/src/constants/Capability.ts
index 321598ee1..6dfd0c703 100644
--- a/src/constants/Capability.ts
+++ b/src/constants/Capability.ts
@@ -1,5 +1,4 @@
-import { NativeModules } from 'react-native';
-const { TrackPlayerModule: TrackPlayer } = NativeModules;
+import TrackPlayer from '../TrackPlayerModule';
export enum Capability {
Play = TrackPlayer.CAPABILITY_PLAY,
diff --git a/src/constants/PitchAlgorithm.ts b/src/constants/PitchAlgorithm.ts
index 56e7da6ca..028395c8a 100644
--- a/src/constants/PitchAlgorithm.ts
+++ b/src/constants/PitchAlgorithm.ts
@@ -1,5 +1,4 @@
-import { NativeModules } from 'react-native';
-const { TrackPlayerModule: TrackPlayer } = NativeModules;
+import TrackPlayer from '../TrackPlayerModule';
export enum PitchAlgorithm {
/**
diff --git a/src/constants/RatingType.ts b/src/constants/RatingType.ts
index 4e834a068..56af9a1c2 100644
--- a/src/constants/RatingType.ts
+++ b/src/constants/RatingType.ts
@@ -1,5 +1,4 @@
-import { NativeModules } from 'react-native';
-const { TrackPlayerModule: TrackPlayer } = NativeModules;
+import TrackPlayer from '../TrackPlayerModule';
export enum RatingType {
Heart = TrackPlayer.RATING_HEART,
diff --git a/src/constants/RepeatMode.ts b/src/constants/RepeatMode.ts
index 06db38d02..5737694bd 100644
--- a/src/constants/RepeatMode.ts
+++ b/src/constants/RepeatMode.ts
@@ -1,5 +1,4 @@
-import { NativeModules } from 'react-native';
-const { TrackPlayerModule: TrackPlayer } = NativeModules;
+import TrackPlayer from '../TrackPlayerModule';
export enum RepeatMode {
/** Playback stops when the last track in the queue has finished playing. */
diff --git a/src/resolveAssetSource.ts b/src/resolveAssetSource.ts
new file mode 100644
index 000000000..2b2f339b3
--- /dev/null
+++ b/src/resolveAssetSource.ts
@@ -0,0 +1,3 @@
+// @ts-expect-error because resolveAssetSource is untyped
+import resolve from 'react-native/Libraries/Image/resolveAssetSource';
+export default resolve;
diff --git a/src/resolveAssetSource.web.ts b/src/resolveAssetSource.web.ts
new file mode 100644
index 000000000..fcd10c1d2
--- /dev/null
+++ b/src/resolveAssetSource.web.ts
@@ -0,0 +1,10 @@
+const resolveAssetResource = (base64: any) => {
+ if (/^https?:\/\//.test(base64)) {
+ return base64;
+ }
+
+ // TODO: resolveAssetResource for web
+ return base64;
+}
+
+export default resolveAssetResource;
diff --git a/src/trackPlayer.ts b/src/trackPlayer.ts
index 337cd46e6..590d1c734 100644
--- a/src/trackPlayer.ts
+++ b/src/trackPlayer.ts
@@ -2,10 +2,10 @@ import {
AppRegistry,
DeviceEventEmitter,
NativeEventEmitter,
- NativeModules,
Platform,
} from 'react-native';
+import TrackPlayer from './TrackPlayerModule';
import { Event, RepeatMode, State } from './constants';
import type {
EventPayloadByEvent,
@@ -18,12 +18,8 @@ import type {
TrackMetadataBase,
UpdateOptions,
} from './interfaces';
+import resolveAssetSource from './resolveAssetSource';
-// the Image.resolveAssetResource is the same as the raw import, but it works with web
-// so use it here. Use a `require` to avoid typescript inconsistencies.
-const resolveAssetSource = require('react-native').Image.resolveAssetSource;
-
-const { TrackPlayerModule: TrackPlayer } = NativeModules;
const emitter =
Platform.OS !== 'android'
? new NativeEventEmitter(TrackPlayer)
@@ -55,6 +51,8 @@ export function registerPlaybackService(factory: () => ServiceHandler) {
if (Platform.OS === 'android') {
// Registers the headless task
AppRegistry.registerHeadlessTask('TrackPlayer', factory);
+ } else if (Platform.OS === 'web') {
+ factory()();
} else {
// Initializes and runs the service in the next tick
setImmediate(factory());
diff --git a/tsconfig.json b/tsconfig.json
index 871cf6e3f..003c897c9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
"esModuleInterop": true,
"isolatedModules": true,
"jsx": "react-native",
- "lib": ["es2017"],
+ "lib": ["es2017", "DOM", "DOM.Iterable"],
"moduleResolution": "node",
"outDir": "./lib",
"strict": true,
diff --git a/web/TrackPlayer/Player.ts b/web/TrackPlayer/Player.ts
new file mode 100644
index 000000000..53b97e01d
--- /dev/null
+++ b/web/TrackPlayer/Player.ts
@@ -0,0 +1,184 @@
+// @ts-ignore
+import shaka from 'shaka-player/dist/shaka-player.ui';
+
+import { State } from '../../src/constants/State';
+import type { Track, Progress, PlaybackState } from '../../src/interfaces';
+
+export class Player {
+ protected element: HTMLMediaElement;
+ protected player: shaka.Player;
+ protected _current?: Track = undefined;
+ protected _playWhenReady: boolean = false;
+ protected _state: PlaybackState = { state: State.None };
+
+ // current getter/setter
+ public get current(): Track | undefined {
+ return this._current;
+ }
+ public set current(cur: Track | undefined) {
+ this._current = cur;
+ }
+
+ // state getter/setter
+ public get state(): PlaybackState {
+ return this._state;
+ }
+ public set state(newState: PlaybackState) {
+ this._state = newState;
+ }
+
+ // playWhenReady getter/setter
+ public get playWhenReady(): boolean {
+ return this._playWhenReady;
+ }
+ public set playWhenReady(pwr: boolean) {
+ this._playWhenReady = pwr;
+ }
+
+ constructor() {
+ // Install built-in polyfills to patch browser incompatibilities.
+ shaka.polyfill.installAll();
+ // Check to see if the browser supports the basic APIs Shaka needs.
+ if (!shaka.Player.isBrowserSupported()) {
+ // This browser does not have the minimum set of APIs we need.
+ this.state = {
+ state: State.Error,
+ error: {
+ code: 'not_supported',
+ message: 'Browser not supported.',
+ },
+ };
+ throw new Error('Browser not supported.');
+ }
+
+ // build dom element and attach shaka-player
+ this.element = document.createElement('audio');
+ this.element.setAttribute('id', 'react-native-track-player');
+ this.player = new shaka.Player(this.element);
+
+ // Listen for relevant events events.
+ this.player.addEventListener('error', (error: any) => {
+ // Extract the shaka.util.Error object from the event.
+ this.onError(error.detail);
+ });
+ this.element.addEventListener('ended', () => this.onTrackEnded());
+ this.element.addEventListener('playing', () => this.onTrackPlaying());
+ this.element.addEventListener('pause', () => this.onTrackPaused());
+ this.player.addEventListener('loading', () => this.onTrackLoading());
+ this.player.addEventListener('loaded', () => this.onTrackLoaded());
+ this.player.addEventListener('buffering', ({ buffering }: any) => {
+ if (buffering === true) {
+ this.onTrackBuffering();
+ }
+ });
+
+ // Attach player to the window to make it easy to access in the JS console.
+ // @ts-ignore
+ window.rntp = this.player;
+ }
+
+ /**
+ * event handlers
+ */
+ protected async onTrackPlaying() {
+ this.state = { state: State.Playing };
+ }
+ protected async onTrackPaused() {
+ this.state = { state: State.Paused };
+ }
+ protected async onTrackLoading() {
+ this.state = { state: State.Loading };
+ }
+ protected async onTrackLoaded() {
+ this.state = { state: State.Ready };
+ }
+ protected async onTrackBuffering() {
+ this.state = { state: State.Buffering };
+ }
+ protected async onTrackEnded() {
+ this.state = { state: State.Ended };
+ }
+ protected onError(error: any) {
+ this.state = {
+ state: State.Error,
+ error: {
+ code: error.code,
+ message: error.message,
+ },
+ };
+
+ // Log the error.
+ console.debug('Error code', error.code, 'object', error);
+ }
+
+ /**
+ * player control
+ */
+ public async load(track: Track) {
+ await this.player.load(track.url);
+ this.current = track;
+ }
+
+ public async retry() {
+ this.player.retryStreaming();
+ }
+
+ public async stop() {
+ this.current = undefined;
+ await this.player.unload()
+ }
+
+ public play() {
+ this.playWhenReady = true;
+ return this.element.play();
+ }
+
+ public pause() {
+ this.playWhenReady = false;
+ return this.element.pause();
+ }
+
+ public setRate(rate: number) {
+ return this.element.playbackRate = rate;
+ }
+
+ public getRate() {
+ return this.element.playbackRate;
+ }
+
+ public seekBy(offset: number) {
+ this.element.currentTime += offset;
+ }
+
+ public seekTo(seconds: number) {
+ this.element.currentTime = seconds;
+ }
+
+ public setVolume(volume: number) {
+ this.element.volume = volume;
+ }
+
+ public getVolume() {
+ return this.element.volume;
+ }
+
+ public getDuration() {
+ return this.element.duration
+ }
+
+ public getPosition() {
+ return this.element.currentTime
+ }
+
+ public getProgress(): Progress {
+ return {
+ position: this.element.currentTime,
+ duration: this.element.duration || 0,
+ buffered: 0, // TODO: this.element.buffered.end,
+ }
+ }
+
+ public getBufferedPosition() {
+ return this.element.buffered.end;
+ }
+}
diff --git a/web/TrackPlayer/PlaylistPlayer.ts b/web/TrackPlayer/PlaylistPlayer.ts
new file mode 100644
index 000000000..b05dac340
--- /dev/null
+++ b/web/TrackPlayer/PlaylistPlayer.ts
@@ -0,0 +1,205 @@
+import { Player } from './Player';
+
+import type { Track } from '../../src/interfaces';
+import {RepeatMode} from './RepeatMode';
+
+export class PlaylistPlayer extends Player {
+ // TODO: use immer to make the `playlist` immutable
+ protected playlist: Track[] = [];
+ protected lastIndex?: number;
+ protected _currentIndex?: number;
+ protected repeatMode: RepeatMode = RepeatMode.Off;
+
+ protected async onTrackEnded() {
+ await super.onTrackEnded();
+ switch (this.repeatMode) {
+ case RepeatMode.Track:
+ if (this.currentIndex !== undefined) {
+ await this.goToIndex(this.currentIndex);
+ }
+ break;
+ case RepeatMode.Playlist:
+ if (this.currentIndex === this.playlist.length - 1) {
+ await this.goToIndex(0);
+ }
+ break;
+ default:
+ try {
+ await this.skipToNext();
+ } catch (err) {
+ if ((err as Error).message !== 'playlist_exhausted') {
+ throw err;
+ }
+
+ this.onPlaylistEnded();
+ }
+ break;
+ }
+ }
+
+ protected onPlaylistEnded() {}
+
+ protected get currentIndex() {
+ return this._currentIndex;
+ }
+
+ protected set currentIndex(current: number | undefined) {
+ this.lastIndex = this.currentIndex;
+ this._currentIndex = current;
+ }
+
+ protected async goToIndex(index: number, initialPosition?: number) {
+ const track = this.playlist[index];
+
+ if (!track) {
+ throw new Error('playlist_exhausted');
+ }
+
+ this.currentIndex = index;
+ await this.load(track);
+
+ if (initialPosition) {
+ await this.seekTo(initialPosition);
+ }
+
+ if (this.playWhenReady) {
+ await this.play();
+ }
+ }
+
+ public async add(tracks: Track[], insertBeforeIndex?: number) {
+ if (insertBeforeIndex) {
+ this.playlist.splice(insertBeforeIndex, 0, ...tracks);
+ } else {
+ this.playlist.push(...tracks);
+ }
+
+ if (this.currentIndex === undefined) {
+ await this.goToIndex(0);
+ }
+ }
+
+ public async skip(index: number, initialPosition?: number) {
+ const track = this.playlist[index];
+
+ if (track === undefined) {
+ throw new Error('index out of bounds');
+ }
+
+ this.currentIndex = index;
+ await this.add([track]);
+
+ if (initialPosition) {
+ await this.seekTo(initialPosition);
+ }
+ }
+
+ public async skipToNext(initialPosition?: number) {
+ if (this.currentIndex === undefined) return;
+
+ const index = this.currentIndex + 1;
+ await this.goToIndex(index, initialPosition);
+ }
+
+ public async skipToPrevious(initialPosition?: number) {
+ if (this.currentIndex === undefined) return;
+
+ const index = this.currentIndex - 1;
+ await this.goToIndex(index, initialPosition);
+ }
+
+ public getTrack(index: number): Track | null {
+ const track = this.playlist[index];
+ return track || null;
+ }
+
+ public setRepeatMode(mode: RepeatMode) {
+ this.repeatMode = mode;
+ }
+
+ public getRepeatMode() {
+ return this.repeatMode;
+ }
+
+ public async remove(indexes: number[]) {
+ const idxMap = indexes.reduce>((acc, elem) => {
+ acc[elem] = true
+ return acc;
+ }, {});
+ let isCurrentRemoved = false;
+ this.playlist = this.playlist.filter((_track, idx) => {
+ const keep = !idxMap[idx]
+
+ if (!keep && idx === this.currentIndex) {
+ isCurrentRemoved = true;
+ }
+
+ return keep;
+ });
+
+ if (this.currentIndex === undefined) {
+ return;
+ }
+
+ const hasItems = this.playlist.length > 0;
+ if (isCurrentRemoved && hasItems) {
+ await this.goToIndex(this.currentIndex % this.playlist.length);
+ } else if (isCurrentRemoved) {
+ await this.stop();
+ }
+ }
+
+ public async stop() {
+ await super.stop();
+ this.currentIndex = undefined;
+ }
+
+ public async reset() {
+ await this.stop();
+ this.playlist = [];
+ }
+
+ public async removeUpcomingTracks() {
+ if (this.currentIndex === undefined) return;
+ this.playlist = this.playlist.slice(0, this.currentIndex + 1);
+ }
+
+ public async move(fromIndex: number, toIndex: number): Promise {
+ if (!this.playlist[fromIndex]) {
+ throw new Error('index out of bounds');
+ }
+
+ if (this.currentIndex === fromIndex) {
+ throw new Error('you cannot move the currently playing track');
+ }
+
+ if (this.currentIndex === toIndex) {
+ throw new Error('you cannot replace the currently playing track');
+ }
+
+ // calculate `currentIndex` after move
+ let shift: number | undefined = undefined;
+ if (this.currentIndex) {
+ if (fromIndex < this.currentIndex && toIndex > this.currentIndex) {
+ shift = -1;
+ } else if (fromIndex > this.currentIndex && toIndex < this.currentIndex) {
+ shift = + 1;
+ }
+ }
+
+ // move the track
+ const fromItem = this.playlist[fromIndex];
+ this.playlist.splice(fromIndex, 1);
+ this.playlist.splice(toIndex, 0, fromItem);
+
+ if (this.currentIndex && shift) {
+ this.currentIndex = this.currentIndex + shift
+ }
+ }
+
+ // TODO
+ public updateMetadataForTrack(index: number, metadata: Partial