Last active
August 30, 2025 13:33
-
-
Save DerGoogler/03be947bb49269dde6489d341710a85d to your computer and use it in GitHub Desktop.
Revisions
-
DerGoogler revised this gist
Aug 30, 2025 . 3 changed files with 86 additions and 101 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -153,9 +153,11 @@ <h2>Select a Song</h2> </div> <script type="module"> import { openInputStream } from "./openInputStream.js"; window.openInputStream = openInputStream const fs = global.require("fs") const fileListEl = document.getElementById('fileList'); const playBtn = document.getElementById('playBtn'); @@ -172,7 +174,7 @@ <h2>Select a Song</h2> let startTime = 0; let isPlaying = false; const files = fs.listSync("/sdcard/Music/Telegram").split(","); files.forEach(file => { const el = document.createElement('div'); el.className = 'file-item'; @@ -196,26 +198,11 @@ <h2>Select a Song</h2> try { const path = `/sdcard/Music/Telegram/${currentFilename}`; const stream = await openInputStream(path) const buffer = await stream.arrayBuffer() audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioBuffer = await audioContext.decodeAudioData(buffer); startPlayback(); } catch (e) { This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,77 @@ export async function openInputStream(path, init = {}) { const mergedInit = { ...readableStreamInit, ...init } return new Promise((resolve, reject) => { const chunks = [] let aborted = false const onAbort = () => { aborted = true cleanup() reject(new DOMException("The operation was aborted.", "AbortError")) } if (mergedInit.signal) { if (mergedInit.signal.aborted) { onAbort() return } mergedInit.signal.addEventListener("abort", onAbort) } function cleanup() { if (mergedInit.signal) { mergedInit.signal.removeEventListener("abort", onAbort) } window.FsInputStream.onmessage = null } window.FsInputStream.addEventListener("message", (event) => { if (aborted) return const msg = event.data if (msg instanceof ArrayBuffer) { chunks.push(new Uint8Array(msg)) } else if (typeof msg === "string") { cleanup() reject(new Error(msg)) return } // Once we have the full file (or a single chunk for now), create a stream const stream = new ReadableStream({ start(controller) { try { for (const chunk of chunks) { controller.enqueue(chunk) } controller.close() cleanup() } catch (e) { cleanup() controller.error(e) } }, cancel(reason) { console.warn("Stream canceled:", reason) cleanup() } }) resolve(new Response(stream, mergedInit)) }) // Send the request to Android window.FsInputStream.postMessage(path) }) } export const readableStreamInit = { headers: { "Content-Type": "application/octet-stream" } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,79 +0,0 @@ -
DerGoogler created this gist
May 11, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,281 @@ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>MMRL Audio Player</title> <!-- Window Safe Area Insets --> <link rel="stylesheet" type="text/css" href="https://mui.kernelsu.org/internal/insets.css" /> <!-- App Theme which the user has currently selected --> <link rel="stylesheet" type="text/css" href="https://mui.kernelsu.org/internal/colors.css" /> <style> * { box-sizing: border-box; } html, body { margin: 0; padding: 0; height: 100%; background: var(--background); color: var(--onBackground); font-family: 'Segoe UI', sans-serif; } body { display: flex; align-items: center; justify-content: center; } .player-card { /* If it doesn't apply: force the apply */ padding-top: calc(var(--window-inset-top, 0px) + 16px) !important; padding-bottom: calc(var(--window-inset-bottom, 0px) + 16px) !important; width: 100%; height: 100%; padding-left: 16px; padding-right: 16px; display: flex; flex-direction: column; align-items: center; background: var(--surfaceContainer); } h2 { margin: 0 0 1rem 0; } .file-list { flex: 1; width: 100%; overflow-y: auto; background: var(--surface); border-radius: 8px; border: 1px solid #3a2a2a; } .file-item { padding: 1rem; border-bottom: 1px solid #2a1a1a; cursor: pointer; } .file-item:hover { background: var(--primaryContainer); } .file-item.active { background: var(--primary); color: var(--onPrimary); font-weight: bold; } .controls { display: flex; gap: 1rem; margin: 1rem 0; } button { padding: 0.75rem 1.5rem; border: none; border-radius: 8px; font-size: 1rem; cursor: pointer; background: var(--filledTonalButtonContainerColor); color: var(--filledTonalButtonContentColor); } button:disabled { opacity: 0.5; cursor: not-allowed; } .loader { border: 4px solid rgba(255, 255, 255, 0.1); border-top: 4px solid var(--primary); border-radius: 50%; width: 32px; height: 32px; animation: spin 1s linear infinite; display: none; } .loader.active { display: block; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #progressContainer { width: 100%; height: 12px; background: #3a2a2a; border-radius: 6px; overflow: hidden; cursor: pointer; margin-top: auto; } #progressBar { height: 100%; width: 0%; background: var(--primary); transition: width 0.1s; } </style> </head> <body> <div class="player-card"> <h2>Select a Song</h2> <div class="file-list" id="fileList"></div> <div class="controls"> <button id="playBtn" disabled>Play</button> <button id="stopBtn" disabled>Stop</button> <div class="loader" id="loader"></div> </div> <div id="progressContainer"> <div id="progressBar"></div> </div> </div> <script type="module"> import { wrapToReadableStream } from "./wrapToReadableStream.mjs"; const fileInterface = window[Object.keys(window).find(key => key.match(/^\$(\w{2})File$/m))]; const fileInputInterface = window[Object.keys(window).find(key => key.match(/^\$(\w{2})FileInputStream$/m))]; const fileListEl = document.getElementById('fileList'); const playBtn = document.getElementById('playBtn'); const stopBtn = document.getElementById('stopBtn'); const loader = document.getElementById('loader'); const progressContainer = document.getElementById('progressContainer'); const progressBar = document.getElementById('progressBar'); let currentFilename = null; let audioContext = null; let sourceNode = null; let animationFrame = null; let audioBuffer = null; let startTime = 0; let isPlaying = false; const files = fileInterface.list("/sdcard/Music/Telegram").split(","); files.forEach(file => { const el = document.createElement('div'); el.className = 'file-item'; el.textContent = file; el.onclick = () => { document.querySelectorAll('.file-item').forEach(i => i.classList.remove('active')); el.classList.add('active'); currentFilename = file; playBtn.disabled = false; stopPlayback(); // auto-stop if switching }; fileListEl.appendChild(el); }); playBtn.onclick = async () => { if (isPlaying) return; playBtn.disabled = true; stopBtn.disabled = false; loader.classList.add('active'); try { const path = `/sdcard/Music/Telegram/${currentFilename}`; const inputStream = fileInputInterface.open(path); const stream = await wrapToReadableStream(inputStream, { chunkSize: 64 * 1024 }); const reader = stream.getReader(); const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const merged = new Uint8Array(chunks.reduce((a, b) => a + b.length, 0)); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.length; } audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioBuffer = await audioContext.decodeAudioData(merged.buffer); startPlayback(); } catch (e) { console.error(e); } finally { loader.classList.remove('active'); } }; stopBtn.onclick = () => { stopPlayback(); playBtn.disabled = false; }; function startPlayback(offset = 0) { if (!audioBuffer) return; sourceNode = audioContext.createBufferSource(); sourceNode.buffer = audioBuffer; sourceNode.connect(audioContext.destination); sourceNode.start(0, offset); startTime = audioContext.currentTime - offset; isPlaying = true; animationFrame = requestAnimationFrame(updateProgress); sourceNode.onended = stopPlayback; } function stopPlayback() { if (sourceNode) { sourceNode.stop(); sourceNode.disconnect(); } if (audioContext) audioContext.close(); if (animationFrame) cancelAnimationFrame(animationFrame); sourceNode = null; audioContext = null; audioBuffer = null; isPlaying = false; stopBtn.disabled = true; progressBar.style.width = '0%'; } function updateProgress() { if (!audioContext || !audioBuffer) return; const elapsed = audioContext.currentTime - startTime; const percent = (elapsed / audioBuffer.duration) * 100; progressBar.style.width = `${Math.min(100, percent)}%`; if (isPlaying) animationFrame = requestAnimationFrame(updateProgress); } progressContainer.onclick = (e) => { if (!audioBuffer || !audioContext) return; const rect = progressContainer.getBoundingClientRect(); const clickX = e.clientX - rect.left; const ratio = clickX / rect.width; const seekTime = ratio * audioBuffer.duration; stopPlayback(); audioContext = new (window.AudioContext || window.webkitAudioContext)(); startPlayback(seekTime); }; </script> </body> </html> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,79 @@ export const defaultStreamOptions = { chunkSize: 1024 * 1024, signal: null, }; export async function wrapToReadableStream(inputStream, options = {}) { const mergedOptions = { ...defaultStreamOptions, ...options }; return new Promise((resolve, reject) => { let input; try { input = inputStream; if (!input) { throw new Error("Failed to open file input stream"); } } catch (error) { reject(error); return; } const abortHandler = () => { try { input?.close(); } catch (error) { console.error("Error during abort cleanup:", error); } reject(new DOMException("The operation was aborted.", "AbortError")); }; if (mergedOptions.signal) { if (mergedOptions.signal.aborted) { abortHandler(); return; } mergedOptions.signal.addEventListener("abort", abortHandler); } const stream = new ReadableStream({ async pull(controller) { try { const chunkData = input.readChunk(mergedOptions.chunkSize); if (!chunkData) { controller.close(); cleanup(); return; } const chunk = JSON.parse(chunkData); if (chunk && chunk.length > 0) { controller.enqueue(new Uint8Array(chunk)); } else { controller.close(); cleanup(); } } catch (error) { cleanup(); controller.error(error); reject(new Error("Error reading file chunk: " + error.message)); } }, cancel() { cleanup(); }, }); function cleanup() { try { if (mergedOptions.signal) { mergedOptions.signal.removeEventListener("abort", abortHandler); } input?.close(); } catch (error) { console.error("Error during cleanup:", error); } } resolve(stream); }); }