Files
v10/docs/ARCHITECTURE.md
T
2025-10-10 21:39:35 -07:00

45 KiB

VJS-10 Architecture & Design Philosophy

How to Read This Document

This document describes VJS-10's architectural principles and patterns, not implementation chronology.

Status Indicators:

  • Implemented - Currently exists in codebase with code references
  • 🚧 In Progress - Partially implemented, under active development
  • 📋 Planned - Architectural vision, not yet started

Code References:

  • File paths link to actual implementation locations
  • Examples show real patterns from working code
  • Sections without status indicators describe foundational principles

Overview

VJS-10 represents a significant architectural evolution in media player component libraries, prioritizing platform-native development experiences while maintaining shared core logic. This document outlines the design philosophy, architectural influences, and key decisions that shape the VJS-10 ecosystem.

Architectural Influences & Inspirations

Media Elements: Platform-Agnostic HTMLMediaElement Contract

VJS-10's media state management architecture draws significant inspiration from the media-elements monorepo, which pioneered the concept of creating HTMLMediaElement-compatible elements that work across different media providers while maintaining consistent interfaces.

1. Extended HTMLMediaElement Contract Foundation

Media Elements Innovation: The media-elements monorepo established the pattern of creating custom elements that "look like" HTMLMediaElement but can be extended for different media providers (HLS, DASH, YouTube, Vimeo, etc.).

Core Architecture Pattern:

// Media Elements: CustomVideoElement extends HTMLVideoElement
export class CustomVideoElement extends HTMLVideoElement implements HTMLVideoElement {
  readonly nativeEl: HTMLVideoElement;

  // Maintains HTMLMediaElement contract
  get currentTime() { return this.nativeEl?.currentTime ?? 0; }
  set currentTime(val) { if (this.nativeEl) this.nativeEl.currentTime = val; }

  play(): Promise<void> { return this.nativeEl?.play() ?? Promise.resolve(); }
  pause(): void { this.nativeEl?.pause(); }
}

// Provider-specific implementations
class HlsVideoElement extends CustomVideoElement {
  api: Hls | null = null;

  async load() {
    if (Hls.isSupported()) {
      this.api = new Hls(this.config);
      this.api.loadSource(this.src);
      this.api.attachMedia(this.nativeEl);
    }
  }
}

Key Architectural Assumptions:

  • Media state owner must be an HTMLElement (DOM-based)
  • Must implement the complete HTMLMediaElement interface
  • Provider-specific logic encapsulated in custom element classes
  • Shadow DOM for consistent styling and behavior

Reference: packages/custom-media-element/custom-media-element.ts

2. VJS-10's Platform-Agnostic Evolution

VJS-10 Innovation: Relaxed the HTMLElement requirement while maintaining the HTMLMediaElement contract, enabling true cross-platform compatibility.

Architectural Relaxation:

// VJS-10: MediaStateOwner - JavaScript interface only
export type MediaStateOwner = Partial<HTMLVideoElement>
  & Pick<HTMLMediaElement, 'play' | 'paused' | 'addEventListener' | 'removeEventListener'>
  & EventTarget & { // Only requires EventTarget, not HTMLElement
    // HTMLMediaElement contract maintained
    currentTime?: number;
    duration?: number;
    volume?: number;
    muted?: boolean;
    paused?: boolean;

    // Extended media-specific properties (Media Elements influence)
    streamType?: StreamTypes;
    targetLiveWindow?: number;
    videoRenditions?: Rendition[] & EventTarget;
    audioTracks?: AudioTrack[] & EventTarget;

    // Platform-specific extensions
    webkitDisplayingFullscreen?: boolean;
    webkitCurrentPlaybackTargetIsWireless?: boolean;
  };

Cross-Platform Implementation:

HTML Platform (Media Elements Heritage):

// Direct evolution from media-elements CustomVideoElement
export class MediaVideoElement extends HTMLVideoElement implements MediaStateOwner {
  connectedCallback() {
    // Media Elements pattern: delegate to native element
    this.mediaStore = createMediaStore(this.nativeEl);
  }
}

React Platform (Platform-Agnostic Contract):

// Uses HTMLVideoElement but not as DOM element
export function useVideoElement(): MediaStateOwner {
  const videoRef = useRef<HTMLVideoElement>(null);

  return useMemo(() => ({
    // Maintains HTMLMediaElement contract without DOM assumptions
    get currentTime() { return videoRef.current?.currentTime ?? 0; },
    set currentTime(val) { if (videoRef.current) videoRef.current.currentTime = val; },

    play: () => videoRef.current?.play() ?? Promise.resolve(),
    pause: () => videoRef.current?.pause(),

    addEventListener: (type, listener) => videoRef.current?.addEventListener(type, listener),
    removeEventListener: (type, listener) => videoRef.current?.removeEventListener(type, listener),
  }), []);
}

React Native Platform (Contract Without HTMLMediaElement):

// Implements MediaStateOwner contract with React Native Video
export function useVideoElementNative(): MediaStateOwner {
  const videoRef = useRef<Video>(null);

  return useMemo(() => ({
    // Same interface, different implementation
    get currentTime() { return this.currentTime ?? 0; },
    set currentTime(val) { videoRef.current?.seek(val); },

    play: () => {
      videoRef.current?.resume();
      return Promise.resolve();
    },
    pause: () => videoRef.current?.pause(),

    // EventTarget implementation for React Native
    addEventListener: (type, listener) => this.eventEmitter.on(type, listener),
    removeEventListener: (type, listener) => this.eventEmitter.off(type, listener),
  }), []);
};

3. Well-Defined Playback Engine Contract

Media Elements Innovation: Beyond creating HTMLMediaElement-compatible custom elements, media-elements established patterns for wrapping diverse playback engines (hls.js, dash.js, Shaka Player) behind a consistent interface. While each integration is currently a "one-off" implementation, the pattern demonstrates the value of a well-defined contract between media elements and the playback engines they wrap.

Current Media Elements Pattern (one-off integrations):

// HlsVideoElement wraps hls.js
class HlsVideoElement extends CustomVideoElement {
  api: Hls | null = null;

  async load() {
    this.api = new Hls(this.config);
    this.api.loadSource(this.src);
    this.api.attachMedia(this.nativeEl);
  }
}

// DashVideoElement wraps dash.js (different API)
class DashVideoElement extends CustomVideoElement {
  player: dashjs.MediaPlayer | null = null;

  async load() {
    this.player = dashjs.MediaPlayer().create();
    this.player.initialize(this.nativeEl, this.src, this.autoplay);
  }
}

Key Observation: Each playback engine has a unique API surface (hls.js's loadSource/attachMedia, dash.js's initialize, Shaka's attach/load), requiring custom integration code for each provider.

VJS-10 Vision: Building on this pattern, VJS-10 aims to define a standardized playback engine contract that enables:

  1. Unified Public-Facing API: All playback engines expose the same interface regardless of internal implementation
  2. Pluggable Providers: Swap between HLS.js, Dash.js, Shaka, or native playback without changing consumer code
  3. Composable Internals: Playback engines composed from functional units rather than monolithic classes

This vision is detailed in docs/PLAYBACK_ENGINE_VISION.md and represents a evolution from media-elements' "one-off integration per provider" pattern to a formalized, extensible playback engine contract.

Contract Philosophy:

  • Outward-facing: Consistent HTMLMediaElement-like API for all providers
  • Inward-facing: Composable functional units for internal implementation
  • Provider-agnostic: Consumer code doesn't need to know about hls.js vs dash.js vs Shaka

Reference: See playback engine vision document for full architectural details.

4. Interface Abstraction vs. Implementation Requirements

Media Elements Approach: Tight coupling between contract and DOM implementation

  • Must extend HTMLVideoElement or HTMLAudioElement
  • Requires Shadow DOM and custom element registration
  • Web Components as the primary abstraction layer

VJS-10 Evolution: Contract-based abstraction with implementation flexibility

  • Interface defines behavior, implementation varies by platform
  • JavaScript properties and methods only (no DOM methods like getAttribute())
  • EventTarget requirement for event-driven architecture

Contract Comparison:

Aspect Media Elements VJS-10 Evolution
Base Class HTMLVideoElement required EventTarget + interface
DOM Requirements Shadow DOM, custom elements None (platform-dependent)
Platform Support Web Components only HTML, React, React Native
HTMLMediaElement Contract Full native implementation JavaScript interface only
Extension Method Class inheritance Interface implementation

5. State Mediation Architecture

Media Elements Pattern: Custom elements as state mediators between providers and native elements:

// Media Elements: Element-centric mediation
class HlsVideoElement extends CustomVideoElement {
  load() {
    // Element mediates between hls.js and native video element
    this.api = new Hls();
    this.api.attachMedia(this.nativeEl); // DOM element required
  }
}

VJS-10 Pattern: State mediators work with any MediaStateOwner:

// VJS-10: Interface-based mediation
export const temporal = {
  currentTime: {
    get(stateOwners: { media: MediaStateOwner }) {
      return stateOwners.media?.currentTime ?? 0; // No DOM assumptions
    },
    set(value: number, stateOwners: { media: MediaStateOwner }) {
      if (stateOwners.media) {
        stateOwners.media.currentTime = value; // JavaScript interface only
      }
    },
    mediaEvents: ['timeupdate', 'loadedmetadata'],
  },
};

6. Event-Driven Architecture Continuity

Shared Foundation: Both systems rely on EventTarget for reactive updates:

Media Elements Implementation:

// DOM event forwarding
class MediaElement {
  // ...
  handleEvent(event: Event): void {
    if (event.target === this.nativeEl) {
      this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
    }
  }
}

VJS-10 Implementation:

// Platform-agnostic event handling
const mediaStateOwner: MediaStateOwner = {
  addEventListener(type: string, listener: EventListener) {
    // Could be DOM element, React ref, or React Native component
    this.targetElement?.addEventListener?.(type, listener)
    || this.eventEmitter?.on?.(type, listener);
  }
};

Media Elements → VJS-10 Evolution Summary

Architecture Layer Media Elements Foundation VJS-10 Platform-Agnostic Evolution
Contract Definition HTMLVideoElement inheritance MediaStateOwner interface
DOM Requirements HTMLElement + Shadow DOM EventTarget only
Platform Support Web Components only Multi-platform (HTML, React, React Native)
State Mediation Element-based providers Interface-based state mediators
Event Architecture DOM event forwarding Platform-agnostic EventTarget
Provider Integration Custom element classes State mediator functions

Key Innovation: VJS-10 maintains the proven HTMLMediaElement contract from media-elements while removing platform-specific constraints, enabling the same state management patterns to work across web components, React components, and React Native.

References:

Media Chrome: Foundational State Management & Media Architecture

VJS-10's core architectural principles are heavily rooted in Media Chrome's pioneering approach to media player component architecture. Media Chrome established several foundational patterns that VJS-10 has evolved and adapted.

1. Extended HTMLMediaElement Contract

Media Chrome Innovation: Built around an "extended HTMLMediaElement" contract that goes beyond standard web APIs to support modern media features.

VJS-10 Evolution: The state mediator pattern directly inherits this concept through MediaStateOwner:

// VJS-10's MediaStateOwner extends HTMLMediaElement concepts
export type MediaStateOwner = Partial<HTMLVideoElement>
  & Pick<
    HTMLMediaElement,
    'play' | 'paused' | 'addEventListener' | 'removeEventListener'
  > & {
    // Media Chrome-inspired extensions
    streamType?: StreamTypes;
    targetLiveWindow?: number;
    liveEdgeStart?: number;
    videoRenditions?: Rendition[] & EventTarget;
    audioTracks?: AudioTrack[] & EventTarget;
    webkitDisplayingFullscreen?: boolean;
    webkitCurrentPlaybackTargetIsWireless?: boolean;
    // ... additional media-specific properties
  };

Key Inheritance:

  • Extensible Media Interface: Beyond basic HTMLMediaElement
  • Event-Driven Architecture: Media element as event source
  • Cross-Platform Abstractions: Handling platform-specific media APIs

Reference: packages/core/media-store/src/state-mediators/audible.ts

2. State Mediator Pattern

Media Chrome Innovation: Originated the state mediator concept as a pattern for managing media state transformations and side effects. Media Chrome's state mediators sit between the raw HTMLMediaElement API and component state, providing a clean abstraction layer.

Original Media Chrome Pattern:

  • Centralized MediaStore with state mediator objects
  • Each mediator handles a specific aspect of media state (volume, time, playback)
  • Mediators encapsulate get/set logic, event subscriptions, and side effects
  • Complete separation of state management from UI components

VJS-10 Evolution: Cleaned up and modularized Media Chrome's state mediator concept:

// VJS-10's modular state mediators (evolved from Media Chrome pattern)
export const audible = {
  muted: {
    get(stateOwners: any) {
      const { media } = stateOwners;
      return media?.muted ?? false;
    },
    set(value: boolean, stateOwners: any) {
      const { media } = stateOwners;
      if (!media) return;
      media.muted = value;
      // Media Chrome-style side effects
      if (!value && !media.volume) {
        media.volume = 0.25;
      }
    },
    mediaEvents: ['volumechange'], // Media Chrome event-driven pattern
    actions: {
      muterequest: () => true,
      unmuterequest: () => false,
    },
  },
};

VJS-10 Improvements:

  • Modular/Composable: State mediators are independent modules (audible, temporal, playable) rather than centralized
  • Framework-Agnostic: Core mediator logic works across platforms (HTML, React, React Native)
  • Cleaner API: Simplified mediator structure with clear get/set/events/actions pattern
  • Type-Safe: Full TypeScript support with inference

Key Architectural Inheritances from Media Chrome:

  • State/UI Separation: State logic completely independent of rendering (Media Chrome's core principle)
  • Event-Driven Updates: Media element events trigger state changes
  • Side Effect Management: Smart defaults (e.g., auto-volume on unmute)
  • Non-Optimistic Updates: Wait for actual media element changes

Reference: packages/core/media-store/src/state-mediators/audible.ts

3. Media-Specific State Abstractions

Media Chrome Contribution: Pioneered media-specific state concepts like mediaVolumeLevel, streamType, and advanced playback states.

VJS-10 Implementation: Organized into specialized state mediators:

// Media Chrome's media-specific state -> VJS-10's organized mediators
export const temporal = {
  currentTime: {
    get(stateOwners: any) {
      const { media } = stateOwners;
      return media?.currentTime ?? 0;
    },
    set(value: number, stateOwners: any) {
      const { media } = stateOwners;
      if (!media || !isValidNumber(value)) return;
      media.currentTime = value;
    },
    mediaEvents: ['timeupdate', 'loadedmetadata'],
    actions: {
      seekrequest: ({ detail = 0 }) => +detail,
    },
  },
  duration: {
    get(stateOwners: any) {
      const { media } = stateOwners;
      return media?.duration ?? 0;
    },
    mediaEvents: ['durationchange', 'loadedmetadata'],
  },
};

Media Chrome Concepts Evolved:

  • Volume Level Abstraction: off, low, medium, high instead of raw numbers
  • Temporal State Management: Time-based controls with smart defaults
  • Playback State Modeling: Beyond simple play/pause

References:

4. Component State Change Side Effects

Media Chrome Pattern: State changes trigger coordinated side effects across the media ecosystem.

VJS-10 Enhancement: Systematic side effect management within state mediators:

// Media Chrome's coordinated side effects -> VJS-10's systematic approach
const mediatorExample = {
  muted: {
    set(value: boolean, stateOwners: any) {
      const { media } = stateOwners;
      if (!media) return;
      media.muted = value;

      // Media Chrome-inspired smart side effects
      if (!value && !media.volume) {
        media.volume = 0.25; // Auto-restore volume on unmute
      }
    },
    // Multiple events can trigger state reevaluation
    mediaEvents: ['volumechange'],
  },

  volume: {
    set(value: number, stateOwners: any) {
      const { media } = stateOwners;
      if (!media || !Number.isFinite(+value)) return;
      media.volume = +value;

      // Coordinated state changes
      if (+value > 0) {
        media.muted = false; // Auto-unmute when volume increased
      }
    },
  },
};

Key Side Effect Patterns:

  • Coordinated State Changes: Volume changes affect mute state
  • Smart Defaults: Reasonable fallbacks for edge cases
  • Event Cascade Management: State changes trigger related updates

5. Event-Driven State Architecture

Media Chrome Foundation: All state changes flow through custom events, creating a reactive system.

VJS-10 Evolution: Maintained event-driven core with enhanced action system:

// Media Chrome's custom events -> VJS-10's action-based system
export const playable = {
  paused: {
    get(stateOwners: any) {
      const { media } = stateOwners;
      return media?.paused ?? true;
    },
    set(value: boolean, stateOwners: any) {
      const { media } = stateOwners;
      if (!media) return;
      if (value) {
        media.pause();
      } else {
        media.play().catch(() => {}); // Media Chrome's error handling pattern
      }
    },
    mediaEvents: ['play', 'pause', 'loadstart'],
    actions: {
      // Media Chrome's request pattern evolved
      playrequest: () => false, // false = not paused
      pauserequest: () => true, // true = paused
    },
  },
};

Event Architecture Evolution:

  • Request/Response Pattern: Components dispatch requests, state mediators handle responses
  • Event Normalization: Consistent patterns across different media APIs
  • Error Handling: Graceful degradation following Media Chrome patterns

6. Web Component Architecture Foundations

Media Chrome Legacy: Established patterns for media-focused web components with Shadow DOM encapsulation.

VJS-10 Platform Evolution: Extended Media Chrome's component concepts across platforms:

HTML Platform (Web Component Heritage):

// Media Chrome's web component pattern -> VJS-10's HTML platform
export class MuteButtonBase extends MediaChromeButton {
  _state: MuteButtonState | undefined;

  handleEvent(event: Event) {
    const { type } = event;
    const state = this._state;
    if (state && type === 'click') {
      // Media Chrome's state-driven interaction pattern
      if (state.volumeLevel === 'off') {
        state.requestUnmute();
      } else {
        state.requestMute();
      }
    }
  }
}

React Platform (Component Pattern Evolution):

// Media Chrome's component logic -> React platform adaptation
export function renderMuteButton(props, state) {
  return (
    <button
      {...props}
      onClick={() => {
      // Same Media Chrome interaction logic, different platform
        if (state.volumeLevel === 'off') {
          state.requestUnmute();
        } else {
          state.requestMute();
        }
      }}
    >
      {props.children}
    </button>
  );
}

Media Chrome → VJS-10 Evolution Summary

Aspect Media Chrome Foundation VJS-10 Evolution
State Management Centralized MediaStore Distributed state mediators
Media Interface Extended HTMLMediaElement Systematic MediaStateOwner
Component Architecture Web Components only Multi-platform (HTML, React, React Native)
State Coupling Event-driven decoupling Hook-based + event-driven
Side Effects Coordinated state changes Systematic mediator side effects
Platform Strategy Web-first with React wrappers Platform-native with shared core

Media Chrome Migration Reference: See MEDIA_CHROME_MIGRATION.md for detailed transformation examples.

Base UI Component Primitives

VJS-10's React component architecture is heavily inspired by Base UI, MUI's headless component library. This influence manifests in several key ways:

1. Primitive Component Philosophy

Base UI Approach: Components are unstyled, behavior-focused primitives that provide functionality without imposing design decisions.

VJS-10 Implementation: All React components follow the primitive pattern, requiring explicit styling and content:

// Primitive approach - no default styling or icons
<MuteButton className={styles.muteButton}>
  <VolumeOffIcon className={styles.icon} />
</MuteButton>;

Reference: packages/react/react/src/components/MuteButton.tsx

2. Render Function Architecture

Base UI Pattern: Components accept custom render props for complete presentation control.

VJS-10 Implementation:

// Built-in render prop support via component factory
function ConnectedComponent({
  render = defaultRender,
  ...props
}: TProps & { render?: TRenderFn }) {
  const connectedState = useStateHook(props);
  const connectedProps = usePropsHook(props, connectedState);
  return render(connectedProps, connectedState);
}

References:

3. Separation of State and Props in Render Functions

Base UI Pattern: Render functions receive computed props and component state as separate arguments.

VJS-10 Implementation:

export function renderMuteButton(props: MuteButtonProps, // Computed props (data attrs, aria, etc.)
  state: MuteButtonState) {
  return (
    <button
      {...props}
      onClick={() => {
        if (state.volumeLevel === 'off') {
          state.requestUnmute();
        } else {
          state.requestMute();
        }
      }}
    >
      {props.children}
    </button>
  );
}

4. Data Attributes for CSS-Driven State Changes

Base UI Philosophy: Expose component state via data attributes for CSS targeting.

VJS-10 Implementation:

export function useMuteButtonProps(props, state) {
  return {
    'data-volume-level': state.volumeLevel, // "off", "low", "medium", "high"
    'data-muted': state.muted ? '' : undefined,
    'aria-label': state.muted ? 'unmute' : 'mute',
    ...props,
  };
}

This enables CSS-driven visual state changes:

[data-volume-level='off'] .volume-high-icon {
  display: none;
}
[data-volume-level='off'] .volume-off-icon {
  display: block;
}
[data-muted] {
  opacity: 0.6;
}

5. Accessibility-First Design

Base UI Approach: Accessibility attributes are built-in by default, not opt-in.

VJS-10 Implementation: All components include accessibility props automatically:

export function useMuteButtonProps(props, state) {
  return {
    role: 'button',
    'aria-label': state.muted ? 'unmute' : 'mute', // Dynamic based on state
    ...props, // External props can override
  };
}

6. Children Expected in All Use Cases

Base UI Philosophy: Even default implementations require explicit content provision.

VJS-10 Example: The default media skin must explicitly provide all styling and content:

// Even "default" skin requires explicit icons and styling
<MuteButton className={`${styles.Button} ${styles.MediaMuteButton}`}>
  <VolumeHighIcon className={`${styles.Icon} ${styles.VolumeHighIcon}`} />
  <VolumeLowIcon className={`${styles.Icon} ${styles.VolumeLowIcon}`} />
  <VolumeOffIcon className={`${styles.Icon} ${styles.VolumeOffIcon}`} />
</MuteButton>;

Reference:

7. Compound Component Architecture

Status: Implemented

Inspiration: Base UI Slider compound components (Slider.Root, Slider.Track, Slider.Thumb)

VJS-10 implements Base UI-inspired compound components for sliders, providing fine-grained control over each sub-component:

TimeSlider Implementation:

<TimeSlider.Root className={styles.SliderRoot}>
  <TimeSlider.Track className={styles.SliderTrack}>
    <TimeSlider.Progress className={styles.SliderProgress} />
    <TimeSlider.Pointer className={styles.SliderPointer} />
  </TimeSlider.Track>
  <TimeSlider.Thumb className={styles.SliderThumb} />
</TimeSlider.Root>;

Components:

  • TimeSlider.Root - Root container with state management and ARIA attributes
  • TimeSlider.Track - Track element for visual rail
  • TimeSlider.Progress - Filled portion showing current progress
  • TimeSlider.Pointer - Preview/hover indicator (media-specific enhancement)
  • TimeSlider.Thumb - Draggable thumb control

VolumeSlider Implementation:

<VolumeSlider.Root orientation="vertical" className={styles.SliderRoot}>
  <VolumeSlider.Track className={styles.SliderTrack}>
    <VolumeSlider.Progress className={styles.SliderProgress} />
  </VolumeSlider.Track>
  <VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>;

Components:

  • VolumeSlider.Root - Root container with volume state
  • VolumeSlider.Track - Track element
  • VolumeSlider.Progress - Filled portion showing current volume
  • VolumeSlider.Thumb - Draggable thumb control

Media-Specific Enhancements:

Unlike Base UI's generic sliders, VJS-10 sliders include media-specific features:

  • TimeSlider.Pointer: Hover preview indicator for scrubbing
  • CSS Variables: --slider-fill, --slider-pointer for styling
  • Data Attributes: data-orientation, data-current-time, data-duration
  • Media State Integration: Automatic synchronization with media playback

Architectural Benefits:

  1. Maximum Flexibility: Fine-grained control over each sub-component
  2. Base UI Consistency: Familiar compound component API patterns
  3. Styling Granularity: Target specific sub-components with CSS selectors
  4. Accessibility: Built-in ARIA relationships between compound components
  5. Primitive Philosophy: Maintains unstyled, behavior-focused approach

References:

VidStack: Framework-Agnostic Common Core Architecture

Primary Influence: VJS-10's multi-platform architecture draws its most significant inspiration from VidStack's framework-agnostic common core pattern—a fundamental departure from the "thin wrapper" approach used by libraries like Media Chrome.

1. Framework-Agnostic Common Core (Primary Influence)

VidStack's Key Innovation: Unlike Media Chrome (which wraps Web Components for React), VidStack built a true shared common core using their Maverick library that provides framework-agnostic UI logic. This means the same business logic, state management, and component behaviors work across Web Components AND React without wrappers or translations.

Why This Matters:

  • Media Chrome Approach: Web Component → React wrapper (thin abstraction, two implementations)
  • VidStack Approach: Common Core → Web Component adaptation + React adaptation (shared logic)
  • Result: VidStack avoids duplication while Media Chrome must maintain framework-specific logic

VJS-10 Adoption: This philosophy directly shaped VJS-10's core package architecture:

// VidStack's common core concept -> VJS-10's core packages targeting multiple runtimes
// @vjs-10/media-store - Framework-agnostic state management
// @vjs-10/playback-engine - Runtime-independent media abstractions
// @vjs-10/media - HTMLMediaElement contracts usable across platforms

// HTML Platform Implementation
export class MediaButton extends HTMLElement {
  connectedCallback() {
    this.mediaStore = getMediaStore(); // Shared core state
  }
}

// React Platform Implementation
export function useMediaButton() {
  const mediaStore = useMediaStore(); // Same core state, different hook
  return mediaStore.getState();
};

// React Native Platform Implementation
export function MediaButtonNative() {
  const store = useMediaStore(); // Same core, native platform
  return <Pressable onPress={store.handlePress} />;
};

Key Architectural Benefits:

  • Code Sharing: Core logic shared across HTML, React, React Native platforms
  • Framework Abstraction: State management independent of rendering framework
  • Unified Developer Experience: Consistent APIs across different platforms

Reference: VJS-10's strict dependency hierarchy prevents circular dependencies while enabling core package reuse

2. Documentation-Based Copy-and-Own Philosophy

VidStack Approach: While not providing CLI tooling like shadcn/ui, VidStack accomplishes component ownership through comprehensive copy-paste functionality in its hosted documentation.

VidStack Implementation:

  • Code Snippet Distribution: Provides copy-paste functionality with snippet IDs (e.g., docs/main) for lazy-loading examples
  • Example-Driven Development: Maintains a dedicated examples repository showing implementation patterns across frameworks
  • Developer Ownership via Documentation: Enables component customization through well-documented, copy-pasteable code patterns

VJS-10 Planned Evolution: VidStack's documentation-first approach influenced the decision to combine traditional npm packages with CLI-based copy-and-own tooling:

# Planned VJS-10 CLI (shadcn/ui inspired, VidStack documentation philosophy)
npx vjs-10 add mute-button --framework=react --skin=default
npx vjs-10 add time-range --framework=html --customizable=true

Hybrid Distribution Model:

  • Traditional npm: Standard package consumption for rapid prototyping
  • Copy-and-Own CLI: Full component source code ownership for production customization
  • Documentation Examples: VidStack-style copy-paste patterns for learning and integration

3. Cross-Platform Component Patterns

VidStack's Framework Strategy: Base components define core logic independent of rendering, then adapt to specific frameworks via Host(Component, HTMLElement) for Web Components and createReactComponent(Component) for React.

VJS-10 Implementation: While not using VidStack's exact adaptation pattern, the philosophical approach directly influenced VJS-10's component architecture:

// VidStack's base component concept -> VJS-10's shared state definitions
// Both separate behavior from presentation

// VidStack Pattern
class BaseButton {
  onSetup() {
    /* core logic */
  }

  onAttach() {
    /* platform attachment */
  }
}
const WebButton = Host(BaseButton, HTMLElement);
const ReactButton = createReactComponent(BaseButton);

// VJS-10 Pattern
const muteButtonStateDefinition = {
  stateTransform: state => ({
    muted: state.muted,
    volumeLevel: state.volumeLevel,
  }),
  createRequestMethods: dispatch => ({
    requestMute: () => dispatch({ type: 'mute' }),
    requestUnmute: () => dispatch({ type: 'unmute' }),
  }),
};

// HTML Platform
export class MuteButtonHTML extends HTMLElement {
  connectedCallback() {
    this.state = muteButtonStateDefinition.stateTransform(
      getMediaStore().getState(),
    );
  }
}

// React Platform
export function useMuteButtonState() {
  const mediaState = useMediaSelector(muteButtonStateDefinition.stateTransform);
  const methods = muteButtonStateDefinition.createRequestMethods(dispatch);
  return { ...mediaState, ...methods };
}

Component Architecture Evolution:

Aspect VidStack Approach VJS-10 Evolution
Base Components Framework-agnostic base classes Framework-agnostic state definitions
Framework Adaptation Host functions & createReactComponent Platform-specific hook implementations
State Management Maverick Signals nanostores with shared transformations
Lifecycle Management onSetup/onAttach/onConnect/onDestroy Standard platform lifecycles

VidStack → VJS-10 Architectural Summary

Influence Area VidStack Foundation VJS-10 Evolution
Framework-Agnostic Common Core (PRIMARY) Maverick-based common core Core packages with strict dependency hierarchy
Component Distribution Documentation-based copy-paste Hybrid npm + CLI copy-and-own (planned)
Framework Adaptation Host functions + createReactComponent Platform-specific implementations sharing core logic
State Management Maverick Signals nanostores with state mediators

Key Takeaway: VidStack's most important influence on VJS-10 is the framework-agnostic common core pattern, which enables sharing UI logic across HTML, React, and React Native without thin wrappers or duplication.

References:

Adobe React Spectrum: State/Behavior/UI Hook Separation

Primary Influence: VJS-10's hook-based component architecture draws its most significant inspiration from Adobe React Spectrum's three-layer hook separation pattern, which cleanly divides component logic into state, behavior, and UI layers.

Adobe Spectrum Three-Layer Hook Architecture

Adobe's Philosophy: "Split each component into three parts: state, behavior, and the rendered component, made possible by React Hooks."

The Three Layers:

  1. State Hooks (React Stately) - "Implements state management and core logic for each component" - Platform-independent, no view system assumptions
  2. Behavior Hooks (React Aria) - "Implements event handling, accessibility, internationalization" - Platform-specific interactions, returns props to spread
  3. Component Layer (React Spectrum) - Renders actual platform elements with design system styling

Why This Separation Matters:

  • State hooks can work across web, React Native, and other platforms
  • Behavior hooks handle web-specific concerns (keyboard, mouse, screen readers)
  • Components provide design system-specific styling without reimplementing logic

Reference: Adobe Spectrum Architecture Documentation

VJS-10's Adaptation: Hook-Based Component Factory

Explicit Acknowledgment: VJS-10's component factory directly references Adobe Spectrum as inspiration:

/**
 * Generic factory function to create connected components following the hooks pattern
 * inspired by Adobe React Spectrum and Base UI architectures.
 */
export function toConnectedComponent(useStateHook, // State layer - media-specific state management
  usePropsHook, // Behavior layer - props transformation & accessibility
  defaultRender, // Component layer - platform-specific rendering
  displayName) {
  /* ... */
}

Reference: packages/react/react/src/utils/component-factory.tsx:20-27

Layer-by-Layer Implementation

1. State Layer: Framework-Agnostic Core Logic

Adobe Spectrum Pattern: State hooks "make no assumptions about the view system" and implement "core logic for the component."

VJS-10 Implementation: State hooks connect to framework-agnostic core packages:

export function useMuteButtonState(_props: any) {
  const mediaStore = useMediaStore();
  const mediaState = useMediaSelector(
    muteButtonStateDefinition.stateTransform, // Core package logic
    shallowEqual,
  );

  const methods = React.useMemo(
    () => muteButtonStateDefinition.createRequestMethods(mediaStore.dispatch),
    [mediaStore],
  );

  return {
    volumeLevel: mediaState.volumeLevel, // Framework-agnostic state
    muted: mediaState.muted,
    requestMute: methods.requestMute, // Framework-agnostic methods
    requestUnmute: methods.requestUnmute,
  } as const;
}

Key Characteristics:

  • Uses core muteButtonStateDefinition from @vjs-10/media-store
  • No platform-specific assumptions
  • Could theoretically work with React Native or other platforms

Reference: packages/react/react/src/components/MuteButton.tsx:10-29

2. Behavior Layer: Props Transformation & Accessibility

Adobe Spectrum Pattern: Behavior hooks "implement event handling, accessibility, internationalization" and "return platform specific props that can be spread onto elements."

VJS-10 Implementation: Props hooks transform state into platform-specific attributes:

export function useMuteButtonProps(props: React.PropsWithChildren<{ [k: string]: any }>, state: ReturnType<typeof useMuteButtonState>) {
  const baseProps: Record<string, any> = {
    // Accessibility (Adobe Spectrum influence)
    role: 'button',
    'aria-label': state.muted ? 'unmute' : 'mute',

    // Platform-specific data attributes
    'data-volume-level': state.volumeLevel,
    'data-tooltip': state.muted ? 'Unmute' : 'Mute',

    // Prop spreading pattern (Adobe Spectrum)
    ...props,
  };

  // Boolean data attribute handling
  if (state.muted) {
    baseProps['data-muted'] = '';
  }

  return baseProps;
}

Key Adobe Spectrum Parallels:

  • Prop Spreading: Returns props object to be spread onto DOM elements
  • Accessibility Focus: ARIA attributes built-in by default
  • Platform Specificity: Handles React/DOM-specific attribute patterns
  • State Integration: Takes state hook output as input parameter

Reference: packages/react/react/src/components/MuteButton.tsx:34-57

3. Component Layer: Platform-Specific Rendering

Adobe Spectrum Pattern: Components "provide the theme and design system specific logic, and renders the actual platform elements."

VJS-10 Implementation: Render functions compose the layers:

export function renderMuteButton(props: MuteButtonProps, // From behavior layer
  state: MuteButtonState) {
  return (
    <button
      {...props} // Adobe Spectrum prop spreading pattern
      onClick={() => {
        if (state.volumeLevel === 'off') {
          state.requestUnmute(); // Framework-agnostic state methods
        } else {
          state.requestMute();
        }
      }}
    >
      {props.children}
      {' '}
      // Platform-specific content
    </button>
  );
}

Adobe Spectrum Parallels:

  • Prop Spreading: {...props} applies behavior layer props to DOM element
  • State Method Usage: Calls framework-agnostic state methods
  • Platform Elements: Renders actual React/DOM elements (<button>)

Framework-Agnostic Benefits

Adobe Spectrum Goal: "Make reusing behavior across design systems as easy as possible, while allowing full design customizability."

VJS-10 Achievement: The three-layer separation enables:

  1. Cross-Platform State: Core state definitions work across HTML, React, React Native
  2. Reusable Behavior: Props transformation logic could be adapted to other frameworks
  3. Complete Styling Control: No imposed design decisions
  4. Accessibility by Default: ARIA patterns built into behavior layer

Component Factory Pattern

Adobe Spectrum Inspiration: Their architecture enables "complete control over the rendering" while maintaining clean separation.

VJS-10's Factory: Automates the Adobe Spectrum pattern:

// Automated composition of Adobe Spectrum's three layers
export const MuteButton = toConnectedComponent(
  useMuteButtonState, // State layer
  useMuteButtonProps, // Behavior layer
  renderMuteButton, // Component layer
  'MuteButton',
);

This factory pattern systematically applies Adobe Spectrum's architectural principles across all VJS-10 components.

Adobe Spectrum → VJS-10 Summary

Primary Takeaway: Adobe Spectrum's most important influence on VJS-10 is the state/behavior/UI hook separation pattern. This three-layer architecture enables:

  1. State hooks (useMuteButtonState) - Platform-agnostic, reusable across web/React Native
  2. Behavior hooks (useMuteButtonProps) - Platform-specific accessibility and prop transformation
  3. UI layer (renderMuteButton) - Design system rendering without business logic

This clean separation of concerns is implemented systematically across all VJS-10 components via the toConnectedComponent() factory, directly mirroring Adobe Spectrum's architectural philosophy.

References:

Additional Resources


This document will continue to evolve as VJS-10's architecture develops and new influences are incorporated.