Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ tools for doing multimodal interaction research
## Install
Download an installer or binary from [releases](https://github.com/jackft/mumo/releases). Runs on Linux Mac Windows.

## Development

From a fresh clone, install workspace dependencies before running dev or build
commands:

```sh
nvm install
nvm use
corepack enable
pnpm install
pnpm run dev
```

The repo requires Node `>=22.13.0`; `.nvmrc` and `.node-version` both pin
`22.13.0` for common version managers.

If Corepack fails with `Cannot find matching keyid`, update Corepack first and
activate the pinned pnpm version:

```sh
nvm use
npm install -g corepack@latest
corepack enable
corepack prepare pnpm@10.33.0 --activate
pnpm install
```

This project uses the pnpm version declared in `package.json`. `pnpm install`
also runs the prepare step that copies large runtime assets into
`packages/mumo/public/`.

## Builds

The browser app build is relocatable by default. You can copy `packages/mumo/dist/`
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"private": true,
"version": "0.0.1",
"packageManager": "pnpm@10.33.0",
"engines": {
"node": ">=22.13.0"
},
"scripts": {
"clean": "rm -rf packages/*/dist packages/*/dist-lib packages/*/out packages/audio-analysis-wasm/pkg",
"build": "pnpm -r --if-present run build",
Expand Down
95 changes: 60 additions & 35 deletions packages/media-player/src/LinkedMediaDlg.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<script lang="ts">
import type { MediaPlayer } from './MediaPlayer.js'
import type { MediaEntry } from './linked-media-types.js'

const {
open,
players,
entries,
onClose,
onLink,
onLoad,
onRemove,
onOffsetChange,
}: {
open: boolean
players: readonly MediaPlayer[]
entries: MediaEntry[]
onClose: () => void
onLink: () => void
onLoad: (id: string) => void
onRemove: (id: string) => void
onOffsetChange: (id: string, offsetSec: number) => void
} = $props()
Expand All @@ -27,26 +29,28 @@
</div>

<div class="lmd-body">
{#if players.length === 0}
{#if entries.length === 0}
<p class="lmd-empty">No media linked.</p>
{:else}
{#each players as player, i (player.id)}
<div class="lmd-row">
<span class="lmd-name" title={player.state?.filename ?? ''}>{player.state?.filename ?? '(loading…)'}</span>
{#if i > 0}
<label class="lmd-offset-label">
Offset (s)
<input class="lmd-offset" type="number" step="0.01"
value={player.track?.offsetSec ?? 0}
oninput={(e) => onOffsetChange(player.id, parseFloat((e.currentTarget as HTMLInputElement).value) || 0)}
/>
</label>
<button class="lmd-remove" onclick={() => onRemove(player.id)}>Remove</button>
{:else}
<span class="lmd-primary-badge">primary</span>
{/if}
</div>
{/each}
<div class="lmd-grid">
{#each entries as entry (entry.id)}
<span class="lmd-name" class:lmd-name-unloaded={entry.kind === 'unloaded'} title={entry.id}>{entry.name}</span>
<span class="lmd-status">{entry.kind === 'unloaded' ? 'not loaded' : ''}</span>
<label class="lmd-offset-label">
Offset (s)
<input class="lmd-offset" type="number" step="0.01"
value={entry.offsetSec}
oninput={(e) => onOffsetChange(entry.id, parseFloat((e.currentTarget as HTMLInputElement).value) || 0)}
/>
</label>
<div class="lmd-actions">
{#if entry.kind === 'unloaded'}
<button class="lmd-load" onclick={() => onLoad(entry.id)}>Load</button>
{/if}
<button class="lmd-remove" onclick={() => onRemove(entry.id)}>Remove</button>
</div>
{/each}
</div>
{/if}
</div>

Expand All @@ -64,10 +68,9 @@
.lmd-panel {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: #fff; border: 1px solid #d0d0d0; border-radius: 6px;
min-width: 340px; max-width: 500px; z-index: 201;
min-width: 420px; max-width: 580px; z-index: 201;
box-shadow: 0 4px 24px rgba(0,0,0,.12);
color: #222;
font-size: 13px;
color: #222; font-size: 13px;
}
.lmd-header {
display: flex; align-items: center; justify-content: space-between;
Expand All @@ -77,29 +80,51 @@
background: none; border: none; cursor: pointer; opacity: 0.45; font-size: 14px; color: #222;
}
.lmd-close:hover { opacity: 0.9; }
.lmd-body { padding: 10px 14px; display: flex; flex-direction: column; gap: 8px; }
.lmd-body { padding: 10px 14px; }
.lmd-empty { opacity: 0.5; font-size: 13px; margin: 0; }
.lmd-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.lmd-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #333; }
.lmd-primary-badge {
font-size: 11px; color: #777; padding: 1px 6px;
border: 1px solid #ccc; border-radius: 10px;

.lmd-grid {
display: grid;
grid-template-columns: 1fr auto auto auto;
align-items: center;
column-gap: 10px;
row-gap: 8px;
}
.lmd-name {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
color: #333; font-size: 12px; min-width: 0;
}
.lmd-name-unloaded { color: #888; font-style: italic; }
.lmd-status {
font-size: 10px; color: #999; white-space: nowrap;
border: 1px solid transparent; border-radius: 8px; padding: 1px 5px;
}
.lmd-status:not(:empty) { border-color: #ddd; }
.lmd-offset-label {
display: flex; align-items: center; gap: 4px;
white-space: nowrap; color: #555; font-size: 12px;
}
.lmd-offset-label { display: flex; align-items: center; gap: 4px; white-space: nowrap; color: #555; }
.lmd-offset {
width: 70px; font-size: 12px; background: #fff;
width: 62px; font-size: 12px; background: #fff;
border: 1px solid #ccc; border-radius: 3px; padding: 2px 4px; color: #222;
}
.lmd-actions {
display: flex; gap: 4px; justify-content: flex-end;
}
.lmd-load {
background: none; border: 1px solid #1565c0; border-radius: 3px;
color: #1565c0; cursor: pointer; padding: 2px 8px; font-size: 11px; white-space: nowrap;
}
.lmd-load:hover { background: #e3f2fd; }
.lmd-remove {
background: none; border: 1px solid #e57373; border-radius: 3px;
color: #c62828; cursor: pointer; padding: 2px 8px; font-size: 11px;
color: #c62828; cursor: pointer; padding: 2px 8px; font-size: 11px; white-space: nowrap;
}
.lmd-remove:hover { background: #c62828; color: #fff; }
.lmd-footer { padding: 10px 14px; border-top: 1px solid #eee; }
.lmd-link-btn {
background: none; border: 1px solid #1565c0; border-radius: 4px;
padding: 5px 14px; cursor: pointer; font-size: 12px; width: 100%;
color: #1565c0;
padding: 5px 14px; cursor: pointer; font-size: 12px; width: 100%; color: #1565c0;
}
.lmd-link-btn:hover { background: #e3f2fd; }
</style>
1 change: 1 addition & 0 deletions packages/media-player/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type { SignalCallbacks } from './SignalBroker.js'
export { default as MediaPlayerView } from './MediaPlayerView.svelte'
export { default as VideoTileLayout } from './VideoTileLayout.svelte'
export { default as LinkedMediaDlg } from './LinkedMediaDlg.svelte'
export type { MediaEntry } from './linked-media-types.js'

export { VideoRenderer } from './VideoRenderer.js'
export { PlayerController } from './PlayerController.js'
Expand Down
3 changes: 3 additions & 0 deletions packages/media-player/src/linked-media-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type MediaEntry =
| { kind: 'loaded'; id: string; name: string; offsetSec: number }
| { kind: 'unloaded'; id: string; name: string; offsetSec: number }
Loading
Loading