Refactor project structure and APIs#72
Open
itzmanish wants to merge 13 commits into
Open
Conversation
- Add draft-16 WebTransport protocol negotiation and request ID flow control - Align control/data wire helpers with draft-16 parameter and extension encoding - Add subscription filter serialization and expose filter options on subscribe - Fix playback startup so media subscriptions begin after catalog/init readiness - Incorporate browser audio decoder and player lifecycle fixes - Add focused transport wire-format tests
Add lib/common/logger.ts — a zero-dependency, single-dispatcher logger:
- LogLevel type: "*" | "trace" | "debug" | "info" | "warn" | "error" | "none"
- Logger interface: optional level() + per-level methods; library calls level()
before every emit so consumers own their own filtering
- Default: built-in console logger at "error" level
- setGlobalLogger(logger) / setGlobalLogger(undefined) to install or restore
- createConsoleLogger(level) helper for consumers who just want a different
verbosity on the built-in output
- notifyLoggerLevelChanged() to re-sync workers when level changes at runtime
- getLogger() — called once at module level with no args; derives scope
automatically from the call stack (e.g. "transport/connection"), returns a
frozen ScopedLogger that closes over scope and calls the single dispatcher
- Worker propagation (Option 1 — custom loggers see worker logs):
- getWorkerLogger() / getWorkerLogger() for Web Worker and AudioWorklet
- setWorkerLogLevel() / setWorkletLogLevel() wired to ToWorker.logLevel and
worklet message.logLevel
- installWorkerLogReceiver() / installWorkletLogReceiver() on the main thread
route FromWorker.log records into the global logger
- Backend sends initial log level and re-syncs on every setGlobalLogger call
via onLoggerLevelChange()
- All output prefixed with "[MoQJS]" and "[<scope>]" as leading args so
consumers can filter or format independently
Migrate all console.* call sites in lib/** to scoped loggers:
- error: caught failures, decoder/encoder errors, fatal stream errors
- warn: dropped chunks/frames, missing elements, unsupported APIs
- debug: lifecycle transitions, setup details, subscribe/publish state
- trace: per-object/per-frame chatty logs, protocol payload dumps
Re-export public logger API (setGlobalLogger, getGlobalLogger,
createConsoleLogger, notifyLoggerLevelChanged, Logger, LogLevel) from all
three entry points so consumers of any bundle variant can configure logging.
…,publisher} - lib/ becomes @moq-js/transport (pure MoQ wire protocol, no media deps) - catalog/ is a new @moq-js/catalog package (pure encode/decode, zero runtime deps) - media/ is a new @moq-js/media package (shared MP4 parse/mux via mp4box) - player/ is a new @moq-js/player package (<video-moq> web component + Player class) - publisher/ is a new @moq-js/publisher package (<publisher-moq> web component + Broadcast) Dependency graph: transport (no runtime deps) catalog (no runtime deps) media -> transport (logger), mp4box player -> transport, catalog, media publisher -> transport, catalog, media Catalog fetch is now an application-layer concern owned by @moq-js/player; the transport layer has no knowledge of .catalog, init tracks, or media flow. Also fix duplicate init-track subscription: Set<[string,string]> was using reference equality so audio+video sharing the same initTrack produced two identical SUBSCRIBE requests. Changed to Map<string, [string,string]> keyed on namespace/initTrack to deduplicate before subscribing.
…d publisher Transport (@moq-js/transport): - SubscribeSend.close(): send UNSUBSCRIBE and drain the data queue (was a no-op) - Subscriber.unsubscribe(): clean up #subscribe, #trackAliasMap, and #aliasToSubscriptionMap via #dropSubscribe before sending UNSUBSCRIBE (previously only #trackToIDMap was cleared) - recvPublishNamespaceDone(): implement instead of throwing TODO; removes namespace from #publishedNamespaces and updates the Watch queue using a new #publishedNamespaceIdMap (id -> namespace key) for lookup - PublishNamespaceSend.close(): send PUBLISH_NAMESPACE_DONE (was a TODO comment); stores the request id on onOk() and uses it on close() - RequestId.applyMaxRequestId(): accept equality as a no-op, throw only on strict decrease (spec allows re-advertising the same value) - Watch.close(): assign a fresh tuple instead of mutating #current[1] in place, eliminating aliasing bugs for callers holding the previous WatchNext reference Player (@moq-js/player): - Player.close(): clear timeupdate interval, await all #trackTasks via Promise.allSettled, await backend.close(), then close the connection - timeupdate interval: tracked as #timeUpdateInterval; started only on play(), stopped on pause() and close(); removed the unconditional start after #run() - Backend: store bound #on handler as #onMessage field and call removeEventListener in close(); store and invoke disposeWorkerLogReceiver() returned by installWorkerLogReceiver so the log forwarding listener is also removed on worker termination - VideoMoq: store document keydown and fullscreenchange handlers as instance fields (keydownHandler, fullscreenChangeHandler) so destroy() removes the identical references — previously anonymous lambdas caused permanent leaks on every mount/unmount cycle - VideoMoq: store PiP pagehide handler as #pipPagehideHandler instance field for the same reason - VideoMoq: disconnectedCallback now tracks the destroy() promise in #destroying and connectedCallback awaits it before starting a new load, preventing overlapping connect/disconnect races
…ttling, transport local teardown
Player/Publisher API:
- Player.create({ logLevel }) and PublisherApi({ logLevel }) install the default
console logger before connecting.
- <video-moq loglevel> and <publisher-moq loglevel> attributes expose the same
configuration to web component consumers.
- @moq-js/player and @moq-js/publisher re-export setGlobalLogger,
getGlobalLogger, createConsoleLogger, notifyLoggerLevelChanged, Logger, and
LogLevel so apps can configure logging without importing @moq-js/transport.
Mute UI:
- VideoMoq updates the mute icon/range optimistically and syncs with the
player's volumechange event so the control reflects the current state even
when the underlying promise has not yet resolved.
Demo:
- demo/config.js gains a logLevel ('debug') and getLogLevel helper.
- demo/index.html and demo/player.html enable the global logger through
window.MoqPlayer / window.MoqPublisher on load and forward both ?relay and
?fingerprint query overrides through to the player page link from the
publisher.
Logging volume control:
- Audio worklet underrun log moves to trace and aggregates ~every 250
process callbacks (~650ms at 48kHz / 128-frame quanta) to avoid flooding
postMessage from the realtime audio thread.
- Worker dropped-audio-samples log moves to trace and aggregates per 9600
samples (~200ms at 48kHz) for the same reason.
Transport (subscriber):
- Subscriber.unsubscribe(): per draft-16 section 5.1.1 ('subscriber keeps
subscription state until it sends UNSUBSCRIBE'), tear down local maps and
close the SubscribeSend data queue immediately after sending UNSUBSCRIBE so
consumers blocked on sub.data() can exit cleanly.
- recvPublishDone(): tolerate already-removed subscriptions (peer may send
PUBLISH_DONE after our UNSUBSCRIBE) instead of throwing into the control
loop.
- recvObject(): drop data destined for a subscription that has already been
removed locally instead of throwing.
Player API (Phase 3.1 "layered factories"):
- Add Player.fromCatalog(connection, catalog, opts) static factory so apps
can share one Connection across multiple players, inspect a catalog
before subscribing, or skip a kind via selection: { audio: null } /
{ video: null }.
- Add TrackSelection and PlayerFromCatalogOptions to the public surface.
selection: { video: 'name' } subscribes to a specific track and throws
if the catalog does not contain a matching kind with that name.
selection: undefined keeps current behavior (first video + first audio).
- Export fetchCatalog(connection, namespace) so apps can inspect a
catalog (track names, codecs, ...) before constructing a Player.
- Player.create now composes fetchCatalog + Player.fromCatalog. Its
public signature (config, tracknum?) is unchanged; tracknum is now an
optional argument with a default of 0.
Logger scope fix:
- getWorkerLogger / getWorkletLogger derive scope from Error().stack URLs.
Workers and worklets bundled via rollup-plugin-web-worker-loader run
from blob: URLs that lack a .ts/.js suffix, so the regex in
_scopeFromUrl returned an empty string and logs emitted
[MoQJS] [] message.
- Provide non-empty defaults: 'worker', 'worklet', and 'main' for the
main-thread getLogger. Explicit scope still wins.
Rollup:
- Add output.exports: 'named' to the player IIFE outputs to silence the
'Mixing named and default exports' warning that fired once fetchCatalog
was added alongside the default Player export. Bundle shape is
unchanged in practice: window.MoqPlayer.default remains VideoMoq and
named exports remain sibling fields.
…udio context
Phase 3.2 "optional canvas / optional audio":
Backend (player/playback/backend.ts):
- Rename internal PlayerConfig to BackendConfig to avoid confusion with
the player-level PlayerConfig and document its contract.
- Add audioTrackName / videoTrackName so the backend knows what the
player is actually going to subscribe to, instead of scanning the
whole catalog and assuming every audio/video track will play.
- Only construct the Audio + RingShared pipeline when audioTrackName is
non-empty (and the named track exists in the catalog with usable
selectionParams).
- Only transfer an OffscreenCanvas to the worker when both a canvas is
provided and videoTrackName is non-empty. transferControlToOffscreen
is not called as a Transferable when there is no video pipeline.
- The worker's onConfig already tolerates a missing audio/video field;
no worker-side change required.
Player API (player/playback/index.ts):
- Mark PlayerConfig.canvas and PlayerFromCatalogOptions.canvas optional
and document that it is required unless selection.video === null.
- Player.fromCatalog now:
+ throws if both audio and video are disabled (nothing to play),
+ throws if a video track is selected but no canvas is provided,
+ only calls transferControlToOffscreen when a video track is
actually selected. transferControlToOffscreen mutates the canvas
element irreversibly, so it must not run for audio-only sessions.
- play() / pause() now guard subscribe + unsubscribe on the per-kind
track name being non-empty, so audio-only and video-only sessions do
not try to subscribe to '' or call connection.unsubscribe('').
Use cases unlocked:
- Audio-only: Player.create({ url, namespace, selection: { video: null } })
or Player.fromCatalog(conn, catalog, { selection: { video: null } }).
- Video-only: pass selection: { audio: null } and a canvas.
- Existing demo path is unchanged: <video-moq> still passes both a canvas
and no explicit selection, defaulting to first video + first audio.
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 characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is based on top of draft-16 branch.