Skip to content

Commit

Permalink
Merge pull request #69 from wnfs-wg/audio
Browse files Browse the repository at this point in the history
docs: Add audio example
  • Loading branch information
icidasset authored Mar 8, 2024
2 parents 2fef0e4 + fd6b49b commit 2cb6881
Show file tree
Hide file tree
Showing 8 changed files with 637 additions and 15 deletions.
10 changes: 10 additions & 0 deletions examples/audio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# demo

## Run locally

From this directory:

```bash
pnpm install
pnpm dev
```
36 changes: 36 additions & 0 deletions examples/audio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "demo",
"type": "module",
"version": "1.0.0",
"private": true,
"description": "",
"author": "Steven Vandevelde <[email protected]> (tokono.ma)",
"license": "MIT",
"keywords": [],
"main": "src/main.jsx",
"scripts": {
"lint": "tsc --build && eslint . && prettier --check '**/*.{js,jsx,ts,tsx,yml,json,css}' --ignore-path ../../.gitignore",
"dev": "rsbuild dev --open",
"build": "rsbuild build",
"preview": "rsbuild preview"
},
"dependencies": {
"@wnfs-wg/nest": "*",
"blockstore-core": "^4.4.0",
"blockstore-idb": "^1.1.8",
"idb-keyval": "^6.2.1",
"interface-blockstore": "^5.2.10",
"mediainfo.js": "^0.2.1",
"mime": "^4.0.1",
"uint8arrays": "^5.0.2"
},
"devDependencies": {
"@rsbuild/core": "^0.4.4",
"@types/node": "^20.11.0",
"typescript": "5.4.2"
},
"eslintConfig": {
"extends": ["@fission-codes"],
"ignorePatterns": ["dist"]
}
}
7 changes: 7 additions & 0 deletions examples/audio/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "@rsbuild/core";

export default defineConfig({
html: {
template: "./src/index.html",
},
});
46 changes: 46 additions & 0 deletions examples/audio/src/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Blockstore } from 'interface-blockstore'

import { CID, FileSystem, Path } from '@wnfs-wg/nest'

import * as IDB from 'idb-keyval'
import * as Uint8Arr from 'uint8arrays'

/////////////////
// FILE SYSTEM //
/////////////////

export async function load({
blockstore,
}: {
blockstore: Blockstore
}): Promise<FileSystem> {
const dataRoot = await IDB.get('fs-pointer')
const storedKey = await IDB.get('capsule-key')

const fs =
dataRoot === undefined
? await FileSystem.create({ blockstore })
: await FileSystem.fromCID(CID.parse(dataRoot), { blockstore })

// Create new or load existing private directory at the root
if (dataRoot && storedKey) {
await fs.mountPrivateNode({
path: Path.root(),
capsuleKey: Uint8Arr.fromString(storedKey, 'base64'),
})
} else {
const { capsuleKey } = await fs.mountPrivateNode({
path: Path.root(),
})

IDB.set('capsule-key', Uint8Arr.toString(capsuleKey, 'base64'))
savePointer(await fs.calculateDataRoot())
}

// Fin
return fs
}

export async function savePointer(dataRoot: CID): Promise<void> {
await IDB.set('fs-pointer', dataRoot.toString())
}
23 changes: 23 additions & 0 deletions examples/audio/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#484A65" />
<meta name="description" content="Demo " />
<title>Demo</title>
</head>
<body>
<input id="file-input" type="file" accept="audio/mp3" />

<div
id="state"
style="
font-family: ui-sans-serif, system-ui, sans-serif;
margin: 1em 0 2em;
"
>
Select an MP3 audio file to add to a WNFS and play it.
</div>
</body>
</html>
180 changes: 180 additions & 0 deletions examples/audio/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { IDBBlockstore } from 'blockstore-idb'
import { Path } from '@wnfs-wg/nest'
import * as FS from './fs.ts'
import mime from 'mime'

// FS
// --

const blockstore = new IDBBlockstore('blockstore')
await blockstore.open()

const fs = await FS.load({ blockstore })

// STATE NODE
// ----------

const state = document.querySelector('#state')
if (!state) throw new Error('Expected a #state element to exist')

function note(msg: string) {
if (state) state.innerHTML = msg
}

// FILE INPUT
// ----------

const fi: HTMLInputElement | null = document.querySelector('#file-input')

if (fi)
fi.addEventListener('change', (event: Event) => {
if (fi?.files?.length !== 1) return
const file: File = fi.files[0]

const reader = new FileReader()

note('Reading file')

reader.onload = (event: any) => {
const data: ArrayBuffer = event.target.result
init(file.name, data)
}

reader.readAsArrayBuffer(file)
})

async function init(fileName: string, fileData: ArrayBuffer) {
const mimeType = mime.getType(fileName)
if (!mimeType || !mimeType.startsWith('audio/'))
throw new Error('Not an audio file')

console.log('Audio mimetype', mimeType)

// File
note('Adding file to WNFS')

const path = Path.file('private', fileName)
const { dataRoot } = await fs.write(path, 'bytes', new Uint8Array(fileData))

FS.savePointer(dataRoot)
const fileSize = await fs.size(path)

// Audio metadata
note('Looking up audio metadata')

const mediainfo = await (
await mediaInfoClient(true)
).analyzeData(
async (): Promise<number> => {
return fileSize
},
async (chunkSize: number, offset: number): Promise<Uint8Array> => {
if (chunkSize === 0) return new Uint8Array()
return fs.read(path, 'bytes', { offset, length: chunkSize })
}
)

// Audio duration
const audioDuration = mediainfo?.media?.track[0]?.Duration
if (!audioDuration) throw new Error('Failed to determine audio duration')

console.log('Audio duration', audioDuration)
console.log('Audio metadata', mediainfo.media.track)

// Buffering
const bufferSize = 512 * 1024 // 512 KB
const metadataSize = mediainfo?.media?.track[0]?.StreamSize

let start = 0
let end = 0
let sourceBuffer: SourceBuffer

async function loadNext() {
if (src.readyState === 'closed' || sourceBuffer.updating) return

if (end >= fileSize) {
note('Loaded all audio data')
if (src.readyState === 'open') src.endOfStream()
return
}

start = end
end =
start === 0
? metadataSize === undefined
? bufferSize
: metadataSize
: start + bufferSize
if (end >= fileSize) end = fileSize

note(`Loading bytes, offset: ${start} - length: ${end - start}`)

const buffer = await fs.read(path, 'bytes', {
offset: start,
length: end - start,
})

sourceBuffer.appendBuffer(buffer)
}

// Media source
note('Setting up media source')

const src = new MediaSource()

src.addEventListener('sourceopen', () => {
if (src.sourceBuffers.length > 0) return
console.log('src.readyState', src.readyState)

if (src.readyState == 'open') {
src.duration = audioDuration

sourceBuffer = src.addSourceBuffer(mimeType)
sourceBuffer.addEventListener('updateend', () => loadNext(), {
once: true,
})

note('Loading initial audio buffer')
loadNext()
}
})

// Create audio
const audio = new Audio()
audio.src = URL.createObjectURL(src)
audio.controls = true
audio.volume = 0.5
// audio.preload = 'metadata'

audio.addEventListener('seeking', () => {
if (src.readyState === 'open') {
// Abort current segment append.
sourceBuffer.abort()
}

// TODO:
// How do we determine what byte offset to load from based on the time.
// start = n

loadNext()
})

audio.addEventListener('progress', () => loadNext())

document.body.appendChild(audio)
}

// AUDIO
// -----

async function mediaInfoClient(covers: boolean) {
const MediaInfoFactory = await import('mediainfo.js').then((a) => a.default)

return await MediaInfoFactory({
coverData: covers,
locateFile: () => {
return new URL('mediainfo.js/MediaInfoModule.wasm', import.meta.url)
.pathname
},
})
}
27 changes: 27 additions & 0 deletions examples/audio/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
"moduleDetection": "force",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["vite/client"],
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"outDir": "dist",
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
// Advanced
"forceConsistentCasingInFileNames": true,
// Type Checking
"strict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": false,
"noUnusedLocals": true,
"noUnusedParameters": false,
"skipLibCheck": true
},
"exclude": ["node_modules", "dist", "out"]
}
Loading

0 comments on commit 2cb6881

Please sign in to comment.