Skip to content

Refactor project structure and APIs#72

Open
itzmanish wants to merge 13 commits into
video-dev:mainfrom
itzmanish:feat/refactor-api
Open

Refactor project structure and APIs#72
itzmanish wants to merge 13 commits into
video-dev:mainfrom
itzmanish:feat/refactor-api

Conversation

@itzmanish
Copy link
Copy Markdown
Contributor

This is based on top of draft-16 branch.

- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant