Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

DropTune MVP

LAN-first collaborative music playback for offline local networks.

Current status

  • Phase 1 implemented: local folder indexing, search, queue, and playback.
  • Phase 2 implemented: room create/list/join over local coordinator APIs.
  • Phase 3 baseline implemented: room-scoped websocket sync with authoritative coordinator queue/playback state.
  • Shared protocol and domain types are centralized in workspace packages.

Monorepo structure

apps/
web/ # React + Vite frontend
coordinator/ # Node.js local coordinator daemon
packages/
shared-types/ # Domain models shared by all apps
protocol/ # Control plane event schema/types

Architecture decisions

  • Control plane and data plane are strictly separated in design:
    • Control plane: @droptune/protocol event envelopes sent via ws.
    • Data plane: reserved for WebRTC DataChannel file transfer in later phases.
  • No server DB, no cloud dependencies, no auth.
  • Local-first browser playback uses HTMLAudioElement.
  • Indexed metadata is persisted in IndexedDB (idb) while file blobs stay in local memory only.

Protocol schema (control plane)

Defined in packages/protocol/src/index.ts:

  • JOIN_ROOM, LEAVE_ROOM, ROOM_STATE
  • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
  • PLAY, PAUSE, SEEK, HOST_CHANGE, PLAYBACK_STATE
  • FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL

All messages use:

  • EventEnvelope<TType, TPayload>
  • sentAt timestamp
  • optional requestId for correlation

Zustand store model

apps/web/src/stores:

  • roomStore: room identity + connection status
  • libraryStore: indexed metadata + local file map + search query + hydration/indexing actions
  • queueStore: queue add/remove/reorder
  • playbackStore: playback status/position/duration/current track
  • peerStore: local peer identity and display name
  • transferStore: transfer status placeholder for Phase 5

Coordinator architecture

apps/coordinator/src/index.ts:

  • HTTP:
    • GET /health
    • GET /rooms
    • POST /rooms/create
  • WebSocket:
    • endpoint: /ws
    • room join/leave tracking
    • room-scoped membership routing
    • authoritative queue and playback updates on:
      • QUEUE_ADD, QUEUE_REMOVE, QUEUE_REORDER
      • PLAY, PAUSE, SEEK, PLAYBACK_STATE, HOST_CHANGE
    • room-state snapshots (ROOM_STATE) emitted after authoritative updates
    • signaling relay for FILE_REQUEST, FILE_AVAILABLE, WEBRTC_SIGNAL
  • mDNS advertisement:
    • service _droptune._tcp.local (type droptune, protocol tcp)

WebRTC lifecycle (planned wiring)

  1. Host emits FILE_REQUEST for missing track.
  2. Source peer replies FILE_AVAILABLE.
  3. Offer/answer/ICE exchange via WEBRTC_SIGNAL on control plane.
  4. DataChannel opens with fixed chunk protocol.
  5. Host reconstructs file Blob, creates object URL, plays via HTMLAudioElement.
  6. Transfer status updates go through transferStore.

Playback state machine

Current playback statuses:

  • idle -> no active source
  • loading -> source assigned, waiting for metadata/play
  • playing -> audio running
  • paused -> user or host paused
  • ended -> track finished (auto-advances queue locally)
  • error -> source/playback failed

Transition source:

  • audio element events (play, pause, ended, loadedmetadata, timeupdate)
  • queue actions (Play now)
  • explicit controls (Resume, Pause)

Phase 1 implementation details

  • Folder ingestion:
    • primary: File System Access API showDirectoryPicker
    • fallback: <input type="file" webkitdirectory multiple>
  • Metadata extraction:
    • music-metadata-browser parses title/artist/album/duration
    • fallback to filename when tags are unavailable
  • Supported formats:
    • mp3, flac, wav, m4a, aac
  • Storage:
    • metadata in IndexedDB (tracks store)
    • raw files NOT persisted in IndexedDB during indexing

Run

From workspace root:

pnpm install
pnpm dev:web

Coordinator (in a second terminal):

pnpm dev:coordinator

Checks:

pnpm typecheck
pnpm build

Debugging guidance

  • Web app:
    • verify IndexedDB data in browser devtools (droptune-db -> tracks)
    • verify metadata parser fallbacks on malformed files (title should use filename)
    • verify audio events update store state (status/position/duration)
  • Coordinator:
    • GET /health for daemon status
    • GET /rooms to verify room registry
    • inspect ws payloads in devtools/network (all control events are plain JSON)
    • use dns-sd/Bonjour browser tools to confirm _droptune._tcp.local advertisement

Next incremental phases

  • Phase 4: host-authoritative playback controls and state broadcasts.
  • Phase 5: WebRTC chunked file transfer.
  • Phase 6: local-first track matching before requesting transfer.
  • Phase 7: prefetch next N=2.
  • Phase 8: host migration with room snapshot handoff.

About

Open source media player to connect two devices. One as a player, and other as the DJ.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages