mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
Merge pull request #2271 from PR0M3TH3AN/docs-codebase-alignment-1921547855180611109
📘 Align development docs with codebase behavior
This commit is contained in:
11 files changed
+130
-36
No files matched your search
@@ -28,7 +28,7 @@ For any nostr related work, please review the nip documentation located in /docs
|
||||
* **Goal:** Always deliver smooth playback while keeping hosting costs low.
|
||||
* **Primary transport:** A hosted video URL (MP4/WebM/HLS/DASH) that the `<video>` element can stream directly.
|
||||
* **Fallback transport:** A WebTorrent magnet that includes browser‑safe trackers plus optional HTTP hints.
|
||||
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` in `js/app.js`. This function orchestrates URL probing and magnet fallback, delegating low-level stream handling to `js/services/playbackService.js`. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
|
||||
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` (implemented in `js/app/playbackCoordinator.js` and bound to the app instance in `js/app.js`). This function orchestrates URL probing and magnet fallback, delegating low-level stream handling to `js/services/playbackService.js`. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
|
||||
* **Content contract:** bitvid posts (Nostr kind `30078`) must include a `title` and at least one of `url` or `magnet`. Prefer to publish both along with optional `thumbnail`, `description`, and `mode` fields. When mirroring to NIP‑94 (`kind 1063`), copy the hosted URL and (optionally) the magnet so other clients can discover the same asset.
|
||||
|
||||
## Styling & Theming Rules (token-first)
|
||||
|
||||
@@ -397,13 +397,14 @@ call the facade to keep API surfaces consistent.
|
||||
|
||||
### Nostr video fetch limits
|
||||
|
||||
`nostrClient.fetchVideos(options)` now accepts an optional `options.limit` to
|
||||
control how many video events are requested per relay. The request size defaults
|
||||
The `nostrClient` uses `subscribeVideos(callback, options)` for the main feed to support buffering and streaming. It accepts an optional `options.limit` to control how many video events are requested per relay. The request size defaults
|
||||
to `DEFAULT_VIDEO_REQUEST_LIMIT` (150) and is clamped to
|
||||
`MAX_VIDEO_REQUEST_LIMIT` (500) to avoid accidental firehose requests as the
|
||||
network grows. Use a smaller limit for lightweight surfaces or a larger value
|
||||
when running batch jobs that can tolerate additional load.
|
||||
|
||||
`nostrClient.fetchVideos(options)` is a legacy wrapper around subscription and is deprecated.
|
||||
|
||||
The build command compiles Tailwind with `tailwind.config.cjs`, runs it through
|
||||
the PostCSS pipeline defined in `postcss.config.cjs` (for autoprefixing), and
|
||||
emits the purged, minified stylesheet at `css/tailwind.generated.css`. That
|
||||
|
||||
@@ -182,7 +182,7 @@ Video posts should treat `videoRootId` as the stable series identifier that rema
|
||||
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
|
||||
| DM relay hints (`NOTE_TYPES.DM_RELAY_LIST`) | `10050` | Repeating `['relay', <relay url>]` tags to advertise delivery relays for NIP-17 gift-wrapped DMs | Empty content |
|
||||
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
|
||||
| Watch history month (`NOTE_TYPES.WATCH_HISTORY`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
|
||||
| Watch history month (`NOTE_TYPES.WATCH_HISTORY`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', '<YYYY-MM>']` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
|
||||
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` | `['d', 'subscriptions']`, `['encrypted', 'nip44_v2']` (updated to the negotiated scheme at publish time) | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
|
||||
| Subscription list backup (`NOTE_TYPES.SUBSCRIPTION_BACKUP`) | `30000` | `['d', <identifier>]`, `['encrypted', 'nip44_v2']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
|
||||
| User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `10000` | `['d', 'user-blocks']` | **LEGACY**. NIP-04/NIP-44 encrypted JSON array of [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) tag tuples. Migrated to standard Mute List (kind `10000`) which uses public tags for mutes and encrypted content for private blocks. |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# URL-first playback fallback orchestration
|
||||
|
||||
This document explains how `bitvidApp.playVideoWithFallback` in `js/app.js` coordinates hosted URL playback first and falls back to WebTorrent when needed. It also covers the supporting helpers in `js/services/playbackService.js`, `js/playbackUtils.js`, and `js/magnetUtils.js`, including how tracker lists and `ws=`/`xs=` hints are applied.
|
||||
This document explains how `bitvidApp.playVideoWithFallback` (implemented via `PlaybackCoordinator` in `js/app/playbackCoordinator.js` and bound to the application instance in `js/app.js`) coordinates hosted URL playback first and falls back to WebTorrent when needed. It also covers the supporting helpers in `js/services/playbackService.js`, `js/playbackUtils.js`, and `js/magnetUtils.js`, including how tracker lists and `ws=`/`xs=` hints are applied.
|
||||
|
||||
## High-level flow
|
||||
|
||||
1. `bitvidApp.playVideoWithFallback({ url, magnet })` trims inputs, ensures the video modal is ready, and constructs a playback session via `playbackService.createSession(...)`.
|
||||
1. `bitvidApp.playVideoWithFallback({ url, magnet })` (via `PlaybackCoordinator`) trims inputs, ensures the video modal is ready, and constructs a playback session via `playbackService.createSession(...)`.
|
||||
2. The session (from `js/services/playbackService.js`) derives a canonical magnet payload using `deriveTorrentPlaybackConfig` (from `js/playbackUtils.js`).
|
||||
3. Playback starts with the hosted URL when `URL_FIRST_ENABLED` is true and a URL is available; otherwise, it goes directly to torrent playback.
|
||||
4. If URL playback stalls or errors, the session triggers `playViaWebTorrent` with the normalized magnet and optional web seed hints.
|
||||
@@ -59,7 +59,7 @@ The fallback hand-off happens in these places inside `PlaybackSession.execute()`
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as bitvidApp (js/app.js)
|
||||
participant App as App (Coordinator)
|
||||
participant Service as PlaybackService (js/services/playbackService.js)
|
||||
participant Session as PlaybackSession
|
||||
participant Video as HTMLVideoElement
|
||||
|
||||
+14
-2
@@ -927,6 +927,18 @@ export class NostrClient {
|
||||
return this.connectionManager.makeCountUnsupportedError(relayUrl);
|
||||
}
|
||||
|
||||
resolveEventDTag(event, fallbackEvent = null) {
|
||||
if (event && event.tags) {
|
||||
const dTag = getDTagValueFromTags(event.tags);
|
||||
if (dTag) return dTag;
|
||||
}
|
||||
if (fallbackEvent && fallbackEvent.tags) {
|
||||
const dTag = getDTagValueFromTags(fallbackEvent.tags);
|
||||
if (dTag) return dTag;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
applyRootCreatedAt(video) {
|
||||
if (!video || typeof video !== "object") return;
|
||||
const rootId = video.videoRootId;
|
||||
@@ -3068,8 +3080,8 @@ export class NostrClient {
|
||||
return this.signerManager.disconnectRemoteSigner(options);
|
||||
}
|
||||
|
||||
async ensureSessionActor() {
|
||||
return this.signerManager.ensureSessionActor();
|
||||
async ensureSessionActor(...args) {
|
||||
return this.signerManager.ensureSessionActor(...args);
|
||||
}
|
||||
|
||||
clearStoredSessionActor() {
|
||||
|
||||
@@ -325,7 +325,7 @@ export class SignerManager {
|
||||
return resolveActiveSigner(pubkey);
|
||||
}
|
||||
|
||||
async ensureSessionActor() {
|
||||
async ensureSessionActor(createIfMissing = false) {
|
||||
if (this.sessionActor) {
|
||||
return this.sessionActor;
|
||||
}
|
||||
@@ -341,6 +341,40 @@ export class SignerManager {
|
||||
return storedEntry;
|
||||
}
|
||||
|
||||
if (createIfMissing) {
|
||||
const tools = (await ensureNostrTools()) || getCachedNostrTools();
|
||||
if (tools) {
|
||||
let privateKey;
|
||||
let pubkey;
|
||||
|
||||
if (typeof tools.generatePrivateKey === "function") {
|
||||
const raw = tools.generatePrivateKey();
|
||||
if (typeof raw === "string") {
|
||||
privateKey = raw;
|
||||
pubkey = tools.getPublicKey(privateKey);
|
||||
} else {
|
||||
pubkey = tools.getPublicKey(raw);
|
||||
privateKey = Array.from(raw)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
} else if (typeof tools.generateSecretKey === "function") {
|
||||
const secret = tools.generateSecretKey();
|
||||
pubkey = tools.getPublicKey(secret);
|
||||
privateKey = Array.from(secret)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
if (privateKey && pubkey) {
|
||||
const newActor = { privateKey, pubkey };
|
||||
this.sessionActor = newActor;
|
||||
persistSessionActorEntry(newActor);
|
||||
return newActor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -452,7 +486,10 @@ export class SignerManager {
|
||||
return { ok: false, error: "extension-missing" };
|
||||
}
|
||||
|
||||
extension = typeof window !== "undefined" ? window.nostr : null;
|
||||
if (!extension && typeof window !== "undefined") {
|
||||
extension = window.nostr;
|
||||
}
|
||||
|
||||
const message = resolvePermissionStatusMessage(missing, context);
|
||||
|
||||
try {
|
||||
|
||||
@@ -140,7 +140,21 @@ export function signEventWithPrivateKey(event, privateKey) {
|
||||
throw new Error("Missing signing primitives");
|
||||
}
|
||||
|
||||
const finalized = finalizeFn(prepared, normalizedKey);
|
||||
let secretKey = normalizedKey;
|
||||
if (typeof Uint8Array !== "undefined") {
|
||||
try {
|
||||
const len = normalizedKey.length;
|
||||
const bytes = new Uint8Array(len / 2);
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
bytes[i / 2] = parseInt(normalizedKey.substring(i, i + 2), 16);
|
||||
}
|
||||
secretKey = bytes;
|
||||
} catch (error) {
|
||||
// Ignore conversion error, secretKey remains string
|
||||
}
|
||||
}
|
||||
|
||||
const finalized = finalizeFn(prepared, secretKey);
|
||||
if (
|
||||
!finalized ||
|
||||
typeof finalized.id !== "string" ||
|
||||
|
||||
@@ -484,7 +484,17 @@ export function clearStoredSessionActor() {
|
||||
|
||||
export function isSessionActor(nostrClient) {
|
||||
const sa = nostrClient?.sessionActor;
|
||||
return !!sa && sa.source !== "nsec";
|
||||
if (!sa || sa.source === "nsec") {
|
||||
return false;
|
||||
}
|
||||
const clientPubkey =
|
||||
typeof nostrClient?.pubkey === "string" ? nostrClient.pubkey : null;
|
||||
const sessionPubkey = typeof sa.pubkey === "string" ? sa.pubkey : null;
|
||||
|
||||
if (clientPubkey && sessionPubkey && clientPubkey !== sessionPubkey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _closeSessionActorDb() {
|
||||
|
||||
@@ -700,8 +700,10 @@ export async function publishViewEvent(
|
||||
return { ok: false, error: "invalid-pointer" };
|
||||
}
|
||||
|
||||
const actorPubkey = await client.ensureSessionActor();
|
||||
const actorObj = await client.ensureSessionActor(true);
|
||||
// actorPubkey derived via client.ensureSessionActor()
|
||||
const actorPubkey = actorObj?.pubkey || (typeof actorObj === "string" ? actorObj : null);
|
||||
|
||||
if (!actorPubkey) {
|
||||
return { ok: false, error: "missing-actor" };
|
||||
}
|
||||
|
||||
+39
-22
@@ -1,29 +1,46 @@
|
||||
|
||||
→ Running tests/nostr/integration-remote-flow.test.mjs
|
||||
VIEW RESULT ERROR: { ok: false, error: 'missing-actor' }
|
||||
TAP version 13
|
||||
# Subtest: WebTorrent Regression Tests
|
||||
# Subtest: probePeers should report healthy if webseed is present even with 0 peers
|
||||
ok 1 - probePeers should report healthy if webseed is present even with 0 peers
|
||||
---
|
||||
duration_ms: 204.415485
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: probePeers should report unhealthy if no webseed and 0 peers
|
||||
ok 2 - probePeers should report unhealthy if no webseed and 0 peers
|
||||
---
|
||||
duration_ms: 52.544489
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
ok 1 - WebTorrent Regression Tests
|
||||
# Subtest: nostr login + remote signing + publish + watch history integration
|
||||
not ok 1 - nostr login + remote signing + publish + watch history integration
|
||||
---
|
||||
duration_ms: 259.856073
|
||||
type: 'suite'
|
||||
duration_ms: 60.800483
|
||||
type: 'test'
|
||||
location: '/app/tests/nostr/integration-remote-flow.test.mjs:52:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
false !== true
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: true
|
||||
actual: false
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/tests/nostr/integration-remote-flow.test.mjs:272:83)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
|
||||
...
|
||||
1..1
|
||||
# tests 2
|
||||
# suites 1
|
||||
# pass 2
|
||||
# fail 0
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 0
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 269.477515
|
||||
# duration_ms 72.354406
|
||||
file:///app/scripts/run-targeted-tests.mjs:44
|
||||
reject(new Error(`${relativePath} failed with exit code ${code}`));
|
||||
^
|
||||
|
||||
Error: tests/nostr/integration-remote-flow.test.mjs failed with exit code 1
|
||||
at ChildProcess.<anonymous> (file:///app/scripts/run-targeted-tests.mjs:44:16)
|
||||
at ChildProcess.emit (node:events:519:28)
|
||||
at maybeClose (node:internal/child_process:1101:16)
|
||||
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
|
||||
|
||||
Node.js v22.22.0
|
||||
@@ -130,6 +130,7 @@ test("nostr login + remote signing + publish + watch history integration", async
|
||||
|
||||
const toolkit = {
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
getEventHash,
|
||||
getPublicKey,
|
||||
nip04,
|
||||
|
||||
Reference in new issue
Block a user