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) {} + public clearNowPlayingMetadata() {} + public updateNowPlayingMetadata(metadata: Partial) {} + +} diff --git a/web/TrackPlayer/RepeatMode.ts b/web/TrackPlayer/RepeatMode.ts new file mode 100644 index 000000000..992c7a3b4 --- /dev/null +++ b/web/TrackPlayer/RepeatMode.ts @@ -0,0 +1,6 @@ + +export enum RepeatMode { + Off = 'REPEAT_OFF', + Track = 'REPEAT_TRACK', + Playlist = 'REPEAT_PLAYLIST', +} diff --git a/web/TrackPlayer/index.ts b/web/TrackPlayer/index.ts new file mode 100644 index 000000000..054576783 --- /dev/null +++ b/web/TrackPlayer/index.ts @@ -0,0 +1,3 @@ +export * from './Player'; +export * from './PlaylistPlayer'; +export * from './RepeatMode'; diff --git a/web/TrackPlayerModule.ts b/web/TrackPlayerModule.ts new file mode 100644 index 000000000..190c581c2 --- /dev/null +++ b/web/TrackPlayerModule.ts @@ -0,0 +1,171 @@ +import { DeviceEventEmitter } from 'react-native'; + +import { Event, PlaybackState, State } from '../src'; +import type { Track, UpdateOptions } from '../src'; +import { PlaylistPlayer, RepeatMode } from './TrackPlayer'; + +export class TrackPlayerModule extends PlaylistPlayer { + protected emitter = DeviceEventEmitter; + protected progressUpdateEventInterval: any; + + // Capabilities + public readonly CAPABILITY_PLAY = 'CAPABILITY_PLAY'; + public readonly CAPABILITY_PLAY_FROM_ID = 'CAPABILITY_PLAY_FROM_ID'; + public readonly CAPABILITY_PLAY_FROM_SEARCH = 'CAPABILITY_PLAY_FROM_SEARCH'; + public readonly CAPABILITY_PAUSE = 'CAPABILITY_PAUSE'; + public readonly CAPABILITY_STOP = 'CAPABILITY_STOP'; + public readonly CAPABILITY_SEEK_TO = 'CAPABILITY_SEEK_TO'; + public readonly CAPABILITY_SKIP = 'CAPABILITY_SKIP'; + public readonly CAPABILITY_SKIP_TO_NEXT = 'CAPABILITY_SKIP_TO_NEXT'; + public readonly CAPABILITY_SKIP_TO_PREVIOUS = 'CAPABILITY_SKIP_TO_PREVIOUS'; + public readonly CAPABILITY_JUMP_FORWARD = 'CAPABILITY_JUMP_FORWARD'; + public readonly CAPABILITY_JUMP_BACKWARD = 'CAPABILITY_JUMP_BACKWARD'; + public readonly CAPABILITY_SET_RATING = 'CAPABILITY_SET_RATING'; + public readonly CAPABILITY_LIKE = 'CAPABILITY_LIKE'; + public readonly CAPABILITY_DISLIKE = 'CAPABILITY_DISLIKE'; + public readonly CAPABILITY_BOOKMARK = 'CAPABILITY_BOOKMARK'; + + // States + public readonly STATE_NONE = 'STATE_NONE'; + public readonly STATE_READY = 'STATE_READY'; + public readonly STATE_PLAYING = 'STATE_PLAYING'; + public readonly STATE_PAUSED = 'STATE_PAUSED'; + public readonly STATE_STOPPED = 'STATE_STOPPED'; + public readonly STATE_BUFFERING = 'STATE_BUFFERING'; + public readonly STATE_CONNECTING = 'STATE_CONNECTING'; + + // Rating Types + public readonly RATING_HEART = 'RATING_HEART'; + public readonly RATING_THUMBS_UP_DOWN = 'RATING_THUMBS_UP_DOWN'; + public readonly RATING_3_STARS = 'RATING_3_STARS'; + public readonly RATING_4_STARS = 'RATING_4_STARS'; + public readonly RATING_5_STARS = 'RATING_5_STARS'; + public readonly RATING_PERCENTAGE = 'RATING_PERCENTAGE'; + + // Repeat Modes + public readonly REPEAT_OFF = RepeatMode.Off; + public readonly REPEAT_TRACK = RepeatMode.Track; + public readonly REPEAT_QUEUE = RepeatMode.Playlist; + + // Pitch Algorithms + public readonly PITCH_ALGORITHM_LINEAR = 'PITCH_ALGORITHM_LINEAR'; + public readonly PITCH_ALGORITHM_MUSIC = 'PITCH_ALGORITHM_MUSIC'; + public readonly PITCH_ALGORITHM_VOICE = 'PITCH_ALGORITHM_VOICE'; + + // observe and emit state changes + public get state(): PlaybackState { + return super.state; + } + public set state(newState: PlaybackState) { + super.state = newState; + this.emitter.emit(Event.PlaybackState, newState); + } + + // TODO: this probably does nothing + public setupPlayer(options: any) {} + + public async updateOptions(options: UpdateOptions) { + // clear and reset interval + this.clearUpdateEventInterval(); + if (options.progressUpdateEventInterval) { + this.clearUpdateEventInterval() + this.progressUpdateEventInterval = setInterval( + async () => { + if (this.state.state === State.Playing) { + const progress = await this.getProgress() + this.emitter.emit(Event.PlaybackProgressUpdated, { + ...progress, + track: this.currentIndex, + }); + } + }, + options.progressUpdateEventInterval * 1000, + ) + } + } + + protected clearUpdateEventInterval() { + if (this.progressUpdateEventInterval) { + clearInterval(this.progressUpdateEventInterval); + } + } + + protected async onTrackEnded() { + const position = this.element.currentTime; + await super.onTrackEnded(); + + this.emitter.emit(Event.PlaybackTrackChanged, { + track: this.lastIndex, + position, + nextTrack: this.currentIndex, + }); + } + + protected async onPlaylistEnded() { + await super.onPlaylistEnded(); + this.emitter.emit(Event.PlaybackQueueEnded, { + track: this.currentIndex, + position: this.element.currentTime, + }); + } + + public get playWhenReady(): boolean { + return super.playWhenReady; + } + + public set playWhenReady(pwr: boolean) { + const didChange = pwr !== this._playWhenReady; + super.playWhenReady = pwr; + + if (didChange) { + this.emitter.emit(Event.PlaybackPlayWhenReadyChanged, { playWhenReady: this._playWhenReady }); + } + } + + public getPlayWhenReady(): boolean { + return this.playWhenReady; + } + + public setPlayWhenReady(pwr: boolean): boolean { + this.playWhenReady = pwr; + return this.playWhenReady; + } + + public async load(track: Track) { + const lastTrack = this.current; + const lastPosition = this.element.currentTime; + await super.load(track); + + this.emitter.emit(Event.PlaybackActiveTrackChanged, { + lastTrack, + lastPosition, + lastIndex: this.lastIndex, + index: this.currentIndex, + track, + }); + } + + public getQueue(): Track[] { + return this.playlist; + } + + public getActiveTrack(): Track | undefined { + return this.current; + } + + public getActiveTrackIndex(): number | undefined { + return this.currentIndex; + } + + /** + * @deprecated + * @returns State + */ + public getState(): State { + return this.state.state; + } + + public getPlaybackState(): PlaybackState { + return this.state; + } +}; diff --git a/web/index.ts b/web/index.ts new file mode 100644 index 000000000..98f44bdcf --- /dev/null +++ b/web/index.ts @@ -0,0 +1,4 @@ +import {TrackPlayerModule} from './TrackPlayerModule'; + +const module = new TrackPlayerModule(); +export default module; diff --git a/yarn.lock b/yarn.lock index 7036a07e4..7ca1fcb5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2680,6 +2680,11 @@ electron-to-chromium@^1.4.251: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +eme-encryption-scheme-polyfill@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.1.tgz#91c823ed584e8ec5a9f03a6a676def8f80c57a4c" + integrity sha512-njD17wcUrbqCj0ArpLu5zWXtaiupHb/2fIUQGdInf83GlI+Q6mmqaPGLdrke4savKAu15J/z1Tg/ivDgl14g0g== + emitter-listener@^1.0.1, emitter-listener@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" @@ -6097,6 +6102,13 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +shaka-player@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-4.3.2.tgz#4c255f93ca6f0452231903a4f0fe068b89f43056" + integrity sha512-sHGDKhKJTPeTks5BiASqr+E+daPHrLYSznEasHR2b7VR34D1OcAcVJk3WQXQiV03tWFFydFGHrgxQDmj1hIA+Q== + dependencies: + eme-encryption-scheme-polyfill "^2.1.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"