A gesture-driven, accessibility-first music player — playback is controlled with simple pointer gestures on a large full-surface target instead of small, precise transport buttons.
Live demo: https://unconventional-player.theharshdeepsingh.com
Unconventional Music Player is a full-stack web app (React client + Express/MongoDB API) for uploading, organizing, and playing back personal audio. Its distinguishing idea is a gesture-driven interaction model: rather than aiming a cursor at tiny play/pause/seek controls, the user performs simple gestures anywhere on the player surface — a tap to toggle playback, a press-and-hold to rewind. Media is stored and delivered through Cloudinary so audio is never served from the application server.
Conventional media transport controls are small, densely packed, and demand precise pointer aiming — play, pause, and seek are typically separate buttons only a few pixels wide. That is awkward for anyone who cannot rely on fine pointer accuracy. This project explores a gesture-first alternative: collapse the most common actions (play/pause and rewind) onto one large interaction surface, where the gesture the user performs — a quick tap versus a sustained press-and-hold — determines the action, rather than where they aim. The result is an accessibility-minded control scheme that trades pixel precision for simple, forgiving gestures.
- Make the primary playback actions reachable through simple gestures on a single large target instead of multiple small buttons.
- Distinguish intent through the gesture itself (tap vs. press-and-hold) rather than spatial precision.
- Let the user tune the rewind step to their own preference and persist it.
- Provide a self-hostable, account-based library backed by scalable media storage.
- Tap-anywhere play/pause — a single tap gesture anywhere on the player surface toggles between play and pause.
- Press-and-hold rewind gesture — holding the surface for ~500ms pauses playback and then repeatedly seeks backward by the user's configured interval until release, at which point playback resumes. Implemented via a reusable
useLongClickhook that separates a tap, a hold start, the in-progress hold loop, and release. - Configurable rewind interval — users pick a rewind step (5 / 10 / 15 / 20 seconds); the choice is stored on the user record and mirrored to
localStorage. - Right-click context menu — a custom context menu positioned at the cursor offers Rename and Delete for a media item.
- Account system — register and log in with JWT-based auth; passwords are hashed with bcryptjs and all
/musicAPI routes are gated by an authentication middleware. - Media library CRUD — upload, list, rename, and delete tracks tied to the signed-in user.
- Cloudinary-backed storage — uploaded audio is pushed to Cloudinary and only its URL/metadata are persisted in MongoDB.
Client/server split. The frontend is a Create React App (React 18 + React Router + React-Bootstrap, styled with SCSS). The backend is an Express API over MongoDB via Mongoose. In production (MODE=production) the same Express server also serves the compiled React build and provides an SPA fallback route; in development the client runs on its own dev server and proxies API calls to the backend.
Layered backend. The server follows a route → controller → model structure (api/v1/<resource>/{index,controller,model}.js) with cross-cutting concerns isolated in a services/ layer — cloudinary, jwt, password, logger (winston), and mongooseConnection. Controllers stay thin and delegate persistence and business logic, which keeps the HTTP boundary separate from data access.
Gesture layer as a reusable primitive. The gesture recognition is not hard-wired into the player. The useLongClick hook encapsulates the timing logic that turns raw pointer events into discrete gestures — a threshold to tell a tap from a press-and-hold, a repeating interval while held, and distinct callbacks for gesture start / while-held / finish / tap. The MediaPlayer composes that hook (tap → play/pause, hold → rewind loop) so the same gesture primitive could drive other surfaces. A native <audio controls> element remains available as a conventional fallback.
Cloudinary as the media CDN. Uploads are received with multer to a temporary disk location, streamed to Cloudinary with resource_type: "auto", and the local temp file is removed immediately after. MongoDB stores only fullUrl, providerName, and originalFileName. This keeps large binaries off the application server and lets delivery scale independently of the API.
Scope note: the gestures here are pointer gestures — tap and press-and-hold — distinguished by duration rather than motion or shape. Pointer position is used only to place the right-click context menu. This is the foundation of the gesture-driven control scheme; richer directional/motion gestures and non-pointer input modalities are noted under Future scope.
Frontend: React 18, React Router v6, React-Bootstrap + Bootstrap 5, Font Awesome, SCSS (node-sass), axios, web-vitals, prop-types.
Backend: Node.js, Express 4, Mongoose 6 (MongoDB), Cloudinary SDK, multer (uploads), jsonwebtoken + bcryptjs (auth), cors, morgan + winston (logging), dotenv.
Tooling: Create React App / react-scripts, Testing Library (jest-dom, react, user-event).
- Node.js and npm
- A MongoDB connection (e.g. MongoDB Atlas)
- A Cloudinary account
- The dev, server, and production scripts rely on
concurrently,nodemon, andforever, which are not declared inpackage.json. Install them globally (or add them as dev dependencies) before using those scripts:npm install -g concurrently nodemon forever
npm install
package.jsondefines apostinstallhook that runsnpm run build, so installing also produces a production build.
Create a .env file in the project root:
# Database
MONGODB_ATLAS_CONNECTION_URL=
# Cloudinary
REACT_APP_CLOUDINARY_CLOUD_NAME=
REACT_APP_CLOUDINARY_API_KEY=
REACT_APP_CLOUDINARY_API_SECRET=
REACT_APP_CLOUDINARY_UPLOAD_PRESET=
REACT_APP_CLOUDINARY_FOLDER_NAME=
# Server (optional)
PORT=
MODE=Available scripts (from package.json):
npm run dev:start # run API (nodemon) and client (react-scripts) together via concurrently
npm run server:start # run only the API with nodemon
npm run client:start # run only the React client
npm run build # build the React client into ./build
npm start # build, then start the server in production mode with forever
npm test # run the Testing Library / Jest test runnerIn development the React client proxies API requests to the backend server. In production, set MODE=production so the Express server serves the built client and the SPA fallback.
- Broaden the gesture vocabulary with directional/motion gestures and non-pointer modalities (keyboard, touch, swipe).
- Add fast-forward and a configurable forward step to complement the rewind hold.
- Surface upload progress and richer library views (search, sorting, playlists).
- Replace
console.logdebugging with structured logging end-to-end and add automated tests for the API and theuseLongClickhook. - Strengthen delete into a clear soft/hard delete contract using the existing
isActiveflag.
Built by Harshdeep Singh — https://theharshdeepsingh.com