Init monorepo n move player to it, add test
This commit is contained in:
parent
b3c176946c
commit
5de339ff47
30 changed files with 5929 additions and 30 deletions
|
@ -3,8 +3,8 @@ root = true
|
|||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
56
.eslintrc.json
Normal file
56
.eslintrc.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"plugins": [
|
||||
"@nx"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {
|
||||
"@nx/enforce-module-boundaries": [
|
||||
"error",
|
||||
{
|
||||
"enforceBuildableLibDependency": true,
|
||||
"allow": [],
|
||||
"depConstraints": [
|
||||
{
|
||||
"sourceTag": "*",
|
||||
"onlyDependOnLibsWithTags": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@nx/typescript"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@nx/javascript"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,7 +7,7 @@ tmp
|
|||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
package-lock.json
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
|
|
13
.prettierrc
13
.prettierrc
|
@ -1,3 +1,12 @@
|
|||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
"parser": "typescript",
|
||||
"trailingComma": "none",
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always"
|
||||
}
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode"]
|
||||
"recommendations": [
|
||||
"nrwl.angular-console",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
|
|
19
README.md
19
README.md
|
@ -1,17 +1,20 @@
|
|||
# Euterpe
|
||||
|
||||
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
||||
|
||||
✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨
|
||||
The sole mono-repo for all things Euterpe.
|
||||
|
||||
## Understand this workspace
|
||||
|
||||
Run `nx graph` to see a diagram of the dependencies of the projects.
|
||||
Run `npx nx graph` to see a diagram of the dependencies of the projects.
|
||||
All programs go like this: `npx nx {package script} {package}`
|
||||
|
||||
## Remote caching
|
||||
## Test this workspace
|
||||
|
||||
Run `npx nx connect-to-nx-cloud` to enable [remote caching](https://nx.app) and make CI faster.
|
||||
Run `npx nx server player-web-test` to see the music player in a minimal demo.
|
||||
|
||||
## Further help
|
||||
## Build
|
||||
|
||||
Visit the [Nx Documentation](https://nx.dev) to learn more.
|
||||
Run `npx nx build player` to build the player.
|
||||
|
||||
## Publish
|
||||
|
||||
First build, then `npm publish --access=public`
|
5
babel.config.json
Normal file
5
babel.config.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"babelrcRoots": [
|
||||
"*"
|
||||
]
|
||||
}
|
59
nx.json
59
nx.json
|
@ -7,23 +7,70 @@
|
|||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "lint", "test", "e2e"]
|
||||
"cacheableOperations": [
|
||||
"build",
|
||||
"lint",
|
||||
"test",
|
||||
"e2e"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["production", "^production"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"production",
|
||||
"^production"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/.eslintignore"
|
||||
]
|
||||
},
|
||||
"e2e": {
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production"
|
||||
]
|
||||
}
|
||||
},
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||
"production": ["default"],
|
||||
"sharedGlobals": []
|
||||
"default": [
|
||||
"{projectRoot}/**/*",
|
||||
"sharedGlobals"
|
||||
],
|
||||
"production": [
|
||||
"default",
|
||||
"!{projectRoot}/.eslintrc.json",
|
||||
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||
"!{projectRoot}/tsconfig.spec.json"
|
||||
],
|
||||
"sharedGlobals": [
|
||||
"{workspaceRoot}/babel.config.json"
|
||||
]
|
||||
},
|
||||
"workspaceLayout": {
|
||||
"appsDir": "packages",
|
||||
"libsDir": "packages"
|
||||
},
|
||||
"generators": {
|
||||
"@nx/web:application": {
|
||||
"style": "css",
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "vitest",
|
||||
"e2eTestRunner": "cypress"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
4764
package-lock.json
generated
4764
package-lock.json
generated
File diff suppressed because it is too large
Load diff
31
package.json
31
package.json
|
@ -1,15 +1,36 @@
|
|||
{
|
||||
"name": "@euterpe/source",
|
||||
"name": "@euterpe.js/source",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {},
|
||||
"private": true,
|
||||
"dependencies": {},
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@nx/cypress": "16.2.1",
|
||||
"@nx/eslint-plugin": "16.2.1",
|
||||
"@nx/js": "16.2.1",
|
||||
"@nx/linter": "16.2.1",
|
||||
"@nx/vite": "^16.2.1",
|
||||
"@nx/web": "^16.2.1",
|
||||
"@nx/workspace": "16.2.1",
|
||||
"@types/node": "^20.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@vitest/coverage-c8": "^0.31.0",
|
||||
"@vitest/ui": "^0.31.0",
|
||||
"cypress": "^12.11.0",
|
||||
"eslint": "~8.15.0",
|
||||
"eslint-config-prettier": "8.1.0",
|
||||
"eslint-plugin-cypress": "^2.10.3",
|
||||
"jsdom": "~20.0.3",
|
||||
"nx": "16.2.1",
|
||||
"prettier": "^2.6.2",
|
||||
"typescript": "~5.0.2"
|
||||
"typescript": "~5.0.2",
|
||||
"vite": "^4.3.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.0.2",
|
||||
"vitest": "^0.31.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
33
packages/player-web-test/.eslintrc.json
Normal file
33
packages/player-web-test/.eslintrc.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": [
|
||||
"../../.eslintrc.json"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
35
packages/player-web-test/index.html
Normal file
35
packages/player-web-test/index.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>PlayerWebTest</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
|
||||
<audio src="" id="audio"></audio>
|
||||
<button id="play">Play</button>
|
||||
<button id="pause">Pause</button>
|
||||
<button id="toggle-play">Toggle Pause/Play</button>
|
||||
<p id="current">-:--</p>
|
||||
<input type="range" min="0" max="10" value="0" id="seek" step="0.01">
|
||||
<p id="duration">-:--</p>
|
||||
<span>
|
||||
<input type="range" min="0" max="1" value="1" id="volume" step="0.01">
|
||||
</span>
|
||||
<button id="mute">Mute</button>
|
||||
<button id="unmute">Unmute</button>
|
||||
<button id="toggle-mute">Toggle Mute</button>
|
||||
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
59
packages/player-web-test/project.json
Normal file
59
packages/player-web-test/project.json
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "player-web-test",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "packages/player-web-test/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/vite:build",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/packages/player-web-test"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"mode": "development"
|
||||
},
|
||||
"production": {
|
||||
"mode": "production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/vite:dev-server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "player-web-test:build"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "player-web-test:build:development",
|
||||
"hmr": true
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "player-web-test:build:production",
|
||||
"hmr": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"executor": "@nx/vite:preview-server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "player-web-test:build"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "player-web-test:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "player-web-test:build:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
packages/player-web-test/public/favicon.ico
Normal file
BIN
packages/player-web-test/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
0
packages/player-web-test/src/assets/.gitkeep
Normal file
0
packages/player-web-test/src/assets/.gitkeep
Normal file
56
packages/player-web-test/src/main.ts
Normal file
56
packages/player-web-test/src/main.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||
music_player_builder.start()
|
||||
const music_player = music_player_builder.build()
|
||||
music_player.change_volume(1)
|
||||
|
||||
music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/nuphory - NVISION (EXTENDED MIX).ogg"))
|
||||
.then(() => {
|
||||
let is_seeking = false
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
//const analyser_node = music_player_builder.add_analyser()
|
||||
music_player.play_async()
|
||||
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
||||
})
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e))
|
||||
})
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true;
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => {
|
||||
alert("Failed seeking! " + e)
|
||||
})
|
||||
is_seeking = false
|
||||
})
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.subscribe_to_formatted_duration_time((time) => {
|
||||
document.querySelector("#duration").innerHTML = time
|
||||
document.querySelector("#seek").max = "" + music_player.get_current_duration()
|
||||
})
|
||||
music_player.subscribe_to_formatted_current_time_tick((time) => {
|
||||
document.querySelector("#current").innerHTML = time
|
||||
})
|
||||
music_player.subscribe_to_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek").value = "" + time
|
||||
})
|
||||
|
||||
}, (e) => console.log(e))
|
17
packages/player-web-test/src/styles.css
Normal file
17
packages/player-web-test/src/styles.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
#volume{
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
body {
|
||||
width: 100vw;
|
||||
height:100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.wrapper{
|
||||
width:50vw;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
30
packages/player-web-test/tsconfig.json
Normal file
30
packages/player-web-test/tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
}
|
42
packages/player-web-test/vite.config.ts
Normal file
42
packages/player-web-test/vite.config.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
import viteTsConfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: "../../node_modules/.vite/player-web-test",
|
||||
|
||||
server: {
|
||||
port: 4200,
|
||||
host: "localhost"
|
||||
},
|
||||
|
||||
preview: {
|
||||
port: 4300,
|
||||
host: "localhost"
|
||||
},
|
||||
|
||||
plugins: [
|
||||
viteTsConfigPaths({
|
||||
root: "../../"
|
||||
})
|
||||
],
|
||||
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [
|
||||
// viteTsConfigPaths({
|
||||
// root: '../../',
|
||||
// }),
|
||||
// ],
|
||||
// },
|
||||
|
||||
test: {
|
||||
globals: true,
|
||||
cache: {
|
||||
dir: "../../node_modules/.vitest"
|
||||
},
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"]
|
||||
}
|
||||
})
|
33
packages/player/.eslintrc.json
Normal file
33
packages/player/.eslintrc.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": [
|
||||
"../../.eslintrc.json"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
5
packages/player/package.json
Normal file
5
packages/player/package.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "@euterpe.js/player",
|
||||
"version": "0.0.1",
|
||||
"type": "commonjs"
|
||||
}
|
41
packages/player/project.json
Normal file
41
packages/player/project.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "player",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/player/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/player",
|
||||
"main": "packages/player/src/index.ts",
|
||||
"tsConfig": "packages/player/tsconfig.lib.json",
|
||||
"assets": []
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"command": "node tools/scripts/publish.mjs player {args.ver} {args.tag}",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/linter:eslint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"packages/player/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"'audioContext",
|
||||
""
|
||||
]
|
||||
}
|
527
packages/player/src/index.ts
Normal file
527
packages/player/src/index.ts
Normal file
|
@ -0,0 +1,527 @@
|
|||
export enum SubscribeEvents {
|
||||
CurrentTimeTick,
|
||||
FormattedDurationTick,
|
||||
FormattedCurrentTimeTick,
|
||||
}
|
||||
const PubSub = () => {
|
||||
//el = event listener
|
||||
const el_current_time_tick: Array<(data: any) => void> = []
|
||||
const el_formatted_duration_tick: Array<(data: any) => void> = []
|
||||
const el_formatted_current_time_tick: Array<(data: any) => void> = []
|
||||
|
||||
function subscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||
switch (event_name) {
|
||||
case SubscribeEvents.CurrentTimeTick: {
|
||||
el_current_time_tick.push(func)
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedDurationTick: {
|
||||
el_formatted_duration_tick.push(func)
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||
el_formatted_current_time_tick.push(func)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
function unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||
switch (event_name) {
|
||||
case SubscribeEvents.CurrentTimeTick: {
|
||||
if (el_current_time_tick.includes(func)) {
|
||||
el_current_time_tick.splice(el_current_time_tick.indexOf(func), 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedDurationTick: {
|
||||
if (el_formatted_duration_tick.includes(func)) {
|
||||
el_formatted_duration_tick.splice(el_formatted_duration_tick.indexOf(func), 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||
if (el_formatted_duration_tick.includes(func)) {
|
||||
el_formatted_duration_tick.splice(el_formatted_duration_tick.indexOf(func), 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
function emit(event_name: SubscribeEvents, data: any) {
|
||||
switch (event_name) {
|
||||
case SubscribeEvents.CurrentTimeTick: {
|
||||
el_current_time_tick.forEach((func) => {
|
||||
func(data)
|
||||
})
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedDurationTick: {
|
||||
el_formatted_duration_tick.forEach((func) => {
|
||||
func(data)
|
||||
})
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||
el_formatted_current_time_tick.forEach((func) => {
|
||||
func(data)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
el_current_time_tick,
|
||||
el_formatted_duration_tick,
|
||||
el_formatted_current_time_tick,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
emit
|
||||
}
|
||||
}
|
||||
|
||||
/* For old browsers */
|
||||
declare global {
|
||||
interface Window {
|
||||
webkitAudioContext: typeof AudioContext
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTMLAudioElement, track_i: MediaElementAudioSourceNode, gain_i: GainNode, volume_i: number, current_song_path_i?: string) => {
|
||||
const audio_element: HTMLAudioElement = audio_element_i
|
||||
const audio_context: AudioContext = audio_context_i
|
||||
const track: MediaElementAudioSourceNode = track_i
|
||||
const gain: GainNode = gain_i
|
||||
let current_song_path: string | undefined = current_song_path_i
|
||||
let current_song_duration: number
|
||||
let volume_cache: number = volume_i
|
||||
let volume: number = volume_i
|
||||
let is_playing = false
|
||||
let time = 0
|
||||
const pub_sub = PubSub()
|
||||
|
||||
function mute_toggle() {
|
||||
if (gain.gain.value == 0) {
|
||||
unmute()
|
||||
} else {
|
||||
mute()
|
||||
}
|
||||
}
|
||||
function mute() {
|
||||
volume_cache = gain.gain.value
|
||||
/* Gentler mute, doesn't pop
|
||||
gain.gain.linearRampToValueAtTime(
|
||||
0,
|
||||
audio_context.currentTime + 0.1
|
||||
);*/
|
||||
volume = gain.gain.value = 0
|
||||
}
|
||||
function unmute() {
|
||||
volume = gain.gain.value = volume_cache
|
||||
}
|
||||
function change_volume(volume_i: number) {
|
||||
volume = gain.gain.value = volume_i
|
||||
}
|
||||
/**
|
||||
* Safer seek_async. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||
*/
|
||||
function try_seek_async(new_time: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (track.context.state == "closed" || track.context.state == "suspended") {
|
||||
is_playing = false
|
||||
reject("Can't seek - track not playing")
|
||||
}
|
||||
audio_element.currentTime = new_time
|
||||
resolve(null)
|
||||
/*audio_element.play().then((s) => resolve(s), (r) => {
|
||||
is_playing = false
|
||||
reject(r)
|
||||
})*/
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Can try to seek even if the audio context was suspended or closed. Best to use try_seek_async()
|
||||
*/
|
||||
function seek_async(new_time: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
audio_element.currentTime = new_time
|
||||
resolve(null)
|
||||
/* audio_element.play().then((s) => resolve(s), (r) => {
|
||||
is_playing = false
|
||||
reject(r)
|
||||
})*/
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result.
|
||||
*/
|
||||
function seek(new_time: number) {
|
||||
audio_element.currentTime = new_time
|
||||
audio_element.play().catch((e) => { throw e })
|
||||
}
|
||||
/**
|
||||
* Safer play_toggle_async. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||
*/
|
||||
function try_play_toggle_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
reject("Context closed or suspended")
|
||||
}
|
||||
if (audio_element.paused) {
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
} else {
|
||||
audio_element.pause()
|
||||
is_playing = false
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Can try to play even if the audio context was suspended or closed. Best to use try_play_toggle_async()
|
||||
*/
|
||||
function play_toggle_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
audio_context.resume()
|
||||
}
|
||||
if (audio_element.paused) {
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
} else {
|
||||
audio_element.pause()
|
||||
is_playing = false
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Unsafe, throws error if failed. Use play_toggle_async or try_play_toggle_async unless you don't care about the result.
|
||||
*/
|
||||
function play_toggle() {
|
||||
if (audio_element.paused) {
|
||||
is_playing = true
|
||||
audio_element.play().catch((r) => {
|
||||
is_playing = false
|
||||
throw r
|
||||
})
|
||||
} else {
|
||||
is_playing = false
|
||||
audio_element.pause()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Safer play_async. Normal play will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||
*/
|
||||
function try_play_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (is_playing) reject(Error("Already playing"))
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
reject("Context closed or suspended")
|
||||
}
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Will try to play even if the audio context was suspended or closed. Best to use try_play_async()
|
||||
*/
|
||||
function play_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (is_playing) resolve(null)
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
audio_context.resume()
|
||||
}
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Unsafe, throws error if failed. Use play_async or try_play_async unless you don't care about the result.
|
||||
*/
|
||||
function play() {
|
||||
if (is_playing) return
|
||||
audio_element.play().catch((r) => {
|
||||
is_playing = false
|
||||
throw r
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Safe technically. Even if audioContext is suspended or closed it will pretend that it paused.
|
||||
*/
|
||||
function pause() {
|
||||
audio_element.pause()
|
||||
is_playing = false
|
||||
}
|
||||
/**
|
||||
* Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback
|
||||
*/
|
||||
function try_new_song_async(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
audio_element.src = current_song_path = path
|
||||
//Found out today about this. Such a nice new way to mass remove event listeners!
|
||||
const controller = new AbortController();
|
||||
|
||||
audio_element.addEventListener("canplaythrough", function canplay_listener(s) {
|
||||
//current_song_duration = audio_element.duration
|
||||
controller.abort()
|
||||
resolve(s)
|
||||
}, { signal: controller.signal })
|
||||
|
||||
audio_element.addEventListener("error", function error_listener(e) {
|
||||
controller.abort()
|
||||
reject(e)
|
||||
}, { signal: controller.signal })
|
||||
|
||||
audio_element.addEventListener("abort", function abort_listener(e) {
|
||||
controller.abort()
|
||||
reject(e)
|
||||
}, { signal: controller.signal })
|
||||
|
||||
audio_element.addEventListener("stalled", function stalled_listener(e) {
|
||||
controller.abort()
|
||||
reject(e)
|
||||
}, { signal: controller.signal })
|
||||
is_playing = false
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song_async() unless you don't care about the result
|
||||
*/
|
||||
function new_song(path: string) {
|
||||
audio_element.src = current_song_path = path
|
||||
//current_song_duration = audio_element.duration
|
||||
}
|
||||
/**
|
||||
* Will parse the duration of the song to make it easy to display in UI
|
||||
* If somethings undefined it returns "0:00"
|
||||
*/
|
||||
function get_formatted_duration() {
|
||||
const dur = audio_element.duration
|
||||
current_song_duration = audio_element.duration
|
||||
|
||||
if (dur == 0 || !dur) return "0:00"
|
||||
|
||||
// ~ is Bitwise NOT, equivalent to Math.floor()
|
||||
const hrs = ~~(dur / 3600);
|
||||
const mins = ~~((dur % 3600) / 60);
|
||||
const secs = ~~dur % 60;
|
||||
|
||||
let ret = ""
|
||||
if (hrs > 0) {
|
||||
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
||||
}
|
||||
|
||||
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
||||
ret += "" + secs;
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Will parse the current time of the song to make it easy to display in UI
|
||||
* If somethings undefined it returns "0:00"
|
||||
*/
|
||||
function get_formatted_current_time() {
|
||||
const curr = audio_element.currentTime
|
||||
|
||||
if (curr == 0 || !curr) return "0:00"
|
||||
// ~~ is Bitwise OR, equivalent to Math.floor()
|
||||
const hrs = ~~(curr / 3600);
|
||||
const mins = ~~((curr % 3600) / 60);
|
||||
const secs = ~~curr % 60;
|
||||
|
||||
let ret = ""
|
||||
if (hrs > 0) {
|
||||
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
||||
}
|
||||
|
||||
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
||||
ret += "" + secs;
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Will give current time every animation frame
|
||||
*/
|
||||
function subscribe_to_time_tick(callback: (data: any) => void) {
|
||||
pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback)
|
||||
emit_current_time()
|
||||
}
|
||||
function emit_current_time() {
|
||||
const request_id = requestAnimationFrame(emit_current_time.bind(MusicPlayer))
|
||||
if (audio_element.ended) is_playing = false
|
||||
if (audio_element.paused) is_playing == false
|
||||
// if use reactively changes volume directly
|
||||
gain.gain.value = volume
|
||||
|
||||
time = audio_element.currentTime
|
||||
if (pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id)
|
||||
pub_sub.emit(SubscribeEvents.CurrentTimeTick, time)
|
||||
}
|
||||
/**
|
||||
* Will give formatted current time via get_formatted_current_time() every animation frame
|
||||
*/
|
||||
function subscribe_to_formatted_current_time_tick(callback: (data: any) => void) {
|
||||
pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback)
|
||||
emit_formatted_current_time()
|
||||
}
|
||||
function emit_formatted_current_time() {
|
||||
const request_id = requestAnimationFrame(emit_formatted_current_time.bind(MusicPlayer))
|
||||
const time = get_formatted_current_time()
|
||||
//if (pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id)
|
||||
pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
|
||||
}
|
||||
/**
|
||||
* Will give formatted duration time via get_formatted_duration() every animation frame
|
||||
*/
|
||||
function subscribe_to_formatted_duration_time(callback: (data: any) => void) {
|
||||
pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback)
|
||||
emit_formatted_duration_time()
|
||||
}
|
||||
function emit_formatted_duration_time() {
|
||||
const request_id = requestAnimationFrame(emit_formatted_duration_time.bind(MusicPlayer))
|
||||
const time = get_formatted_duration()
|
||||
//if (pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id)
|
||||
pub_sub.emit(SubscribeEvents.FormattedDurationTick, time)
|
||||
}
|
||||
return {
|
||||
track,
|
||||
get_is_playing: () => is_playing,
|
||||
get_current_path: () => current_song_path,
|
||||
get_current_duration: () => current_song_duration,
|
||||
volume,
|
||||
time,
|
||||
mute,
|
||||
unmute,
|
||||
mute_toggle,
|
||||
change_volume,
|
||||
try_seek_async,
|
||||
seek_async,
|
||||
seek,
|
||||
play,
|
||||
pause,
|
||||
play_toggle,
|
||||
play_toggle_async,
|
||||
try_play_toggle_async,
|
||||
try_new_song_async,
|
||||
new_song,
|
||||
get_formatted_duration,
|
||||
get_formatted_current_time,
|
||||
subscribe_to_formatted_current_time_tick,
|
||||
subscribe_to_formatted_duration_time,
|
||||
subscribe_to_time_tick,
|
||||
try_play_async,
|
||||
play_async,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export function MusicPlayerBuilder(audio_element: HTMLAudioElement) {
|
||||
let audio_context: AudioContext
|
||||
let gain: GainNode
|
||||
let track: MediaElementAudioSourceNode
|
||||
const volume = 1
|
||||
let prev_node: any;
|
||||
let is_gain_connected = false
|
||||
/**
|
||||
* Creates a context and gain( Gets connected at the end )
|
||||
* will throw if audio_element is undefined (stupid vue setup amirite?)
|
||||
* will throw if user has not interacted with the page yet (Can't initiate AudioContext)
|
||||
*/
|
||||
function start() {
|
||||
if (audio_element === undefined) throw Error("audio_element was undefined")
|
||||
// ↓ For old browsers
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
audio_context = new AudioContext()
|
||||
track = audio_context.createMediaElementSource(audio_element)
|
||||
gain = audio_context.createGain()
|
||||
}
|
||||
/**
|
||||
* For external use, not kept inside player after connection.
|
||||
* @returns {AnalyserNode}
|
||||
*/
|
||||
function add_analyser() {
|
||||
const analyser = audio_context.createAnalyser()
|
||||
!prev_node ? track.connect(analyser) : prev_node.connect(analyser)
|
||||
prev_node = analyser
|
||||
return analyser
|
||||
}
|
||||
/**
|
||||
* For external use, not kept inside player after connection.
|
||||
* @returns {StereoPannerNode}
|
||||
*/
|
||||
function add_stereo_panner_node() {
|
||||
const panner = audio_context.createStereoPanner()
|
||||
!prev_node ? track.connect(panner) : prev_node.connect(panner)
|
||||
prev_node = panner
|
||||
return panner
|
||||
}
|
||||
/**
|
||||
* For external use, not kept inside player after connection.
|
||||
* @returns {StereoPannerNode}
|
||||
*/
|
||||
function add_wave_shaper_node() {
|
||||
const shaper = audio_context.createWaveShaper()
|
||||
!prev_node ? track.connect(shaper) : prev_node.connect(shaper)
|
||||
prev_node = shaper
|
||||
return shaper
|
||||
}
|
||||
/**
|
||||
* For additional trickery, you can connect your own node.
|
||||
*/
|
||||
function connect_custom_node(node: AudioNode) {
|
||||
!prev_node ? track.connect(node) : prev_node.connect(node)
|
||||
prev_node = node
|
||||
}
|
||||
/**
|
||||
* Only use if you need to connect the gain before another node,
|
||||
* eg. if you want the analyser nodes output to be affected by user gain
|
||||
*/
|
||||
function connect_gain() {
|
||||
!prev_node ? track.connect(gain) : prev_node.connect(gain)
|
||||
prev_node = gain
|
||||
is_gain_connected = true
|
||||
}
|
||||
/**
|
||||
* Finishes the build
|
||||
* @returns {MusicPlayer: () => void}
|
||||
*/
|
||||
function build() {
|
||||
if (!is_gain_connected) {
|
||||
!prev_node ? track.connect(gain) : prev_node.connect(gain)
|
||||
prev_node = gain
|
||||
}
|
||||
prev_node.connect(audio_context.destination)
|
||||
audio_element.preload = "metadata"
|
||||
return MusicPlayer(audio_context, audio_element, track, gain, volume)
|
||||
}
|
||||
return {
|
||||
start,
|
||||
add_analyser,
|
||||
add_stereo_panner_node,
|
||||
add_wave_shaper_node,
|
||||
connect_gain,
|
||||
connect_custom_node,
|
||||
build
|
||||
}
|
||||
}
|
3
packages/player/src/lib/player.ts
Normal file
3
packages/player/src/lib/player.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function player(): string {
|
||||
return "player"
|
||||
}
|
19
packages/player/tsconfig.json
Normal file
19
packages/player/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
18
packages/player/tsconfig.lib.json
Normal file
18
packages/player/tsconfig.lib.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
65
tools/scripts/publish.mjs
Normal file
65
tools/scripts/publish.mjs
Normal file
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* This is a minimal script to publish your package to "npm".
|
||||
* This is meant to be used as-is or customize as you see fit.
|
||||
*
|
||||
* This script is executed on "dist/path/to/library" as "cwd" by default.
|
||||
*
|
||||
* You might need to authenticate with NPM before running this script.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process"
|
||||
import { readFileSync, writeFileSync } from "fs"
|
||||
import chalk from "chalk"
|
||||
|
||||
import devkit from "@nx/devkit"
|
||||
const { readCachedProjectGraph } = devkit
|
||||
|
||||
function invariant(condition, message) {
|
||||
if (!condition) {
|
||||
console.error(chalk.bold.red(message))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag}
|
||||
// Default "tag" to "next" so we won't publish the "latest" tag by accident.
|
||||
const [, , name, version, tag = "next"] = process.argv
|
||||
|
||||
// A simple SemVer validation to validate the version
|
||||
const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/
|
||||
invariant(
|
||||
version && validVersion.test(version),
|
||||
`No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.`
|
||||
)
|
||||
|
||||
const graph = readCachedProjectGraph()
|
||||
const project = graph.nodes[name]
|
||||
|
||||
invariant(
|
||||
project,
|
||||
`Could not find project "${name}" in the workspace. Is the project.json configured correctly?`
|
||||
)
|
||||
|
||||
const outputPath = project.data?.targets?.build?.options?.outputPath
|
||||
invariant(
|
||||
outputPath,
|
||||
`Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`
|
||||
)
|
||||
|
||||
process.chdir(outputPath)
|
||||
|
||||
// Updating the version in "package.json" before publishing
|
||||
try {
|
||||
const json = JSON.parse(readFileSync(`package.json`).toString())
|
||||
json.version = version
|
||||
writeFileSync(`package.json`, JSON.stringify(json, null, 2))
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.bold.red(
|
||||
`Error reading package.json file from library build output.`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Execute "npm publish" to publish
|
||||
execSync(`npm publish --access public --tag ${tag}`)
|
|
@ -10,11 +10,21 @@
|
|||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
"paths": {
|
||||
"@euterpe/player": [
|
||||
"packages/player/src/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"tmp"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue