commit 4b0d84e9c8adfa7401084389da5deb751420b629 Author: Christian Pillsbury Date: Tue Jul 29 10:41:46 2025 -0700 feat: initialize Video.js 10 monorepo with core architecture - Set up monorepo structure with core, HTML, React, and React Native packages - Add TypeScript configuration with project references - Create foundational packages: media-store, playback-engine, media, icons - Implement platform-specific packages for HTML, React, and React Native - Add comprehensive documentation and development tooling - Include CLAUDE.md with development guidelines and conventional commits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e85d76d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Build outputs +dist/ +build/ +lib/ +out/ +*.tsbuildinfo +.turbo/ + +# Environment variables +.env +.env.* +!.env.example + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +*.sublime-project + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# Coverage reports +coverage/ +*.lcov +.nyc_output/ +.coverage/ + +# Testing +.jest/ +__tests__/coverage/ + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Temporary files +tmp/ +temp/ +.tmp/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/settings.json + +# React Native +.expo/ +.expo-shared/ +.react-native/ + +# Web-specific +.next/ +.nuxt/ +.vuepress/dist/ +.serverless/ +.fusebox/ +.dynamodb/ + +# Package manager lock files (keeping npm for this setup) +# package-lock.json # Uncommented for npm workspace +yarn.lock +pnpm-lock.yaml +bun.lockb + +# Monorepo specific +.rush/ +.turbo/ +.lerna/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..58884dc3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Architecture Overview + +This is a Video.js 10 monorepo organized by platform/runtime with a clear dependency hierarchy: + +### Package Structure +- **Core packages** (`packages/core/*`) - Runtime-agnostic packages that form the foundation +- **HTML packages** (`packages/html/*`) - DOM/Browser-specific implementations +- **React packages** (`packages/react/*`) - React-specific implementations +- **React Native packages** (`packages/react-native/*`) - React Native implementations + +### Dependency Hierarchy +- Core packages have no dependencies on other vjs-10 packages +- HTML packages depend only on core packages +- React packages depend only on core packages (with React peer deps) +- React Native packages depend only on core packages (with React Native peer deps) + +This prevents circular dependencies and ensures maximum reusability. + +### Key Core Packages +- `@vjs-10/media-store` - State management for media players +- `@vjs-10/playback-engine` - Abstraction layer for media engines (HLS.js, Dash.js, etc.) +- `@vjs-10/media` - HTMLMediaElement contracts and utilities +- `@vjs-10/icons` - SVG icon definitions and utilities + +## Common Development Commands + +### Monorepo Commands (run from root) +```bash +# Install all dependencies +npm install + +# Build all packages +npm run build + +# Run tests across all packages +npm run test + +# Type checking across all packages +npm run typecheck + +# Lint all packages +npm run lint + +# Clean all packages +npm run clean +``` + +### Working with Specific Packages +```bash +# Build specific package +npm run build --workspace=@vjs-10/media-store + +# Work in specific package directory +cd packages/core/media-store +npm run build +``` + +## TypeScript Configuration + +The monorepo uses TypeScript project references for efficient compilation: +- `tsconfig.base.json` - Shared compiler options with strict settings +- `tsconfig.json` - Root config with path mappings and project references +- Each package has its own `tsconfig.json` extending the base + +Key TypeScript features: +- Strict mode enabled with additional checks (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`) +- Path mappings for all `@vjs-10/*` packages point to source directories +- Composite builds for incremental compilation + +## Package Development + +### Individual Package Scripts +Most packages follow this pattern: +```bash +npm run build # Compile TypeScript (tsc) +npm run test # Currently placeholder "No tests yet" +npm run clean # Remove dist directory +``` + +### Package Types +- **Core packages** - Pure TypeScript, no external dependencies +- **HTML packages** - May include DOM-specific code, depend on core packages +- **React packages** - Include React peer dependencies, depend on core packages +- **React Native packages** - Include React Native peer dependencies (react-native-video, react-native-svg) + +## Workspace Management + +This uses npm workspaces with the following workspace patterns: +- `packages/core/*` +- `packages/html/*` +- `packages/react/*` +- `packages/react-native/*` + +Internal dependencies use `workspace:*` protocol for linking between packages. + +## Git Workflow + +This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) for commit messages. + +### Commit Message Format +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Common Types +- `feat:` - New feature +- `fix:` - Bug fix +- `docs:` - Documentation changes +- `style:` - Code style changes (formatting, semicolons, etc.) +- `refactor:` - Code refactoring without feature changes +- `test:` - Adding or updating tests +- `chore:` - Maintenance tasks, dependency updates + +### Scope Examples +Use package names or areas of the codebase: +- `feat(media-store): add pause state management` +- `fix(react-icons): resolve SVG rendering issue` +- `docs(readme): update installation instructions` +- `chore(deps): update typescript to 5.4.0` + +### Breaking Changes +For breaking changes, add `!` after the type/scope: +``` +feat!: remove deprecated playback API +feat(media-store)!: change state interface structure +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..1b290fc8 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# VJS-10 Monorepo + +A monorepo for Video.js 10 related library packages, organized by platform and runtime environment. + +## Structure + +``` +vjs-10-monorepo/ +├── packages/ +│ ├── core/ # Runtime-agnostic packages +│ │ ├── media-store/ # State abstraction for media +│ │ ├── playback-engine/# Abstraction for media source management +│ │ ├── media/ # HTMLMediaElement contracts and utilities +│ │ └── icons/ # Environment-agnostic SVG icons +│ ├── html/ # DOM/Browser packages +│ │ ├── html-icons/ # HTML/DOM icon components +│ │ ├── html-media-elements/ # HTML media element components +│ │ ├── html-media-store/ # HTML media store integration +│ │ └── html/ # Complete HTML UI library +│ ├── react/ # React packages +│ │ ├── react-icons/ # React icon components +│ │ ├── react-media-elements/ # React media element components +│ │ ├── react-media-store/ # React media store hooks and context +│ │ └── react/ # Complete React UI library +│ └── react-native/ # React Native packages +│ ├── react-native-icons/ # React Native icon components +│ ├── react-native-media-elements/ # React Native media components +│ └── react-native/ # Complete React Native UI library +└── apps/ # Example applications (coming soon) +``` + +## Packages + +### Core Packages (Runtime Agnostic) + +- **@vjs-10/media-store** - State management for media players +- **@vjs-10/playback-engine** - Abstraction layer for media engines (HLS.js, Dash.js, etc.) +- **@vjs-10/media** - HTMLMediaElement contracts and utilities +- **@vjs-10/icons** - SVG icon definitions and utilities + +### HTML/DOM Packages + +- **@vjs-10/html-icons** - Web component icon elements +- **@vjs-10/html-media-elements** - Web component media elements +- **@vjs-10/html-media-store** - DOM integration for media store +- **@vjs-10/html** - Complete HTML media player UI library + +### React Packages + +- **@vjs-10/react-icons** - React icon components +- **@vjs-10/react-media-elements** - React media element components +- **@vjs-10/react-media-store** - React hooks and context for media state +- **@vjs-10/react** - Complete React media player UI library + +### React Native Packages + +- **@vjs-10/react-native-icons** - React Native icon components (requires react-native-svg) +- **@vjs-10/react-native-media-elements** - React Native media components (requires react-native-video) +- **@vjs-10/react-native** - Complete React Native media player UI library + +## Getting Started + +### Installation + +```bash +npm install +``` + +### Building + +Build all packages: +```bash +npm run build +``` + +Build specific package: +```bash +npm run build --workspace=@vjs-10/core +``` + +### Development + +```bash +# Install dependencies +npm install + +# Build all packages +npm run build + +# Run type checking +npm run typecheck + +# Run linting +npm run lint +``` + +## Package Dependencies + +The packages are designed with clear dependency relationships: + +- **Core packages** have no dependencies on other vjs-10 packages +- **HTML packages** depend only on core packages +- **React packages** depend only on core packages (peer dependency on React) +- **React Native packages** depend only on core packages (peer dependencies on React Native, react-native-video, react-native-svg) + +This ensures maximum reusability and prevents circular dependencies. + +## TypeScript + +All packages are written in TypeScript and use project references for efficient compilation. The monorepo uses a shared `tsconfig.base.json` for consistent compiler options across all packages. + +## License + +Apache-2.0 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..13a0c1d0 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "vjs-10-monorepo", + "version": "1.0.0", + "description": "A monorepo for Video.js 10 related library packages", + "private": true, + "workspaces": [ + "packages/core/*", + "packages/html/*", + "packages/react/*", + "packages/react-native/*" + ], + "scripts": { + "build": "npm run build --workspaces --if-present", + "test": "npm run test --workspaces --if-present", + "lint": "npm run lint --workspaces --if-present", + "typecheck": "npm run typecheck --workspaces --if-present", + "clean": "npm run clean --workspaces --if-present" + }, + "devDependencies": { + "typescript": "^5.3.0", + "@types/node": "^20.0.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/videojs/vjs-10-monorepo.git" + }, + "license": "Apache-2.0", + "author": "Video.js Contributors" +} \ No newline at end of file diff --git a/packages/core/.gitignore b/packages/core/.gitignore new file mode 100644 index 00000000..cf9975e3 --- /dev/null +++ b/packages/core/.gitignore @@ -0,0 +1,14 @@ +# Core packages specific ignores + +# Documentation generation +docs/api/ + +# Benchmark results +benchmark-results/ +performance-logs/ + +# Type generation artifacts +types/generated/ + +# Schema validation artifacts +schemas/compiled/ \ No newline at end of file diff --git a/packages/core/icons/package.json b/packages/core/icons/package.json new file mode 100644 index 00000000..161c432b --- /dev/null +++ b/packages/core/icons/package.json @@ -0,0 +1,29 @@ +{ + "name": "@vjs-10/icons", + "version": "0.1.0", + "description": "Environment-agnostic library of SVG icons and possibly transforms or tooling for them.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "icons", + "svg", + "media", + "player", + "ui" + ], + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/core/icons/src/index.ts b/packages/core/icons/src/index.ts new file mode 100644 index 00000000..580c0d06 --- /dev/null +++ b/packages/core/icons/src/index.ts @@ -0,0 +1,65 @@ +export interface IconDefinition { + name: string; + viewBox: string; + paths: string[]; +} + +export const ICON_DEFINITIONS: Record = { + play: { + name: 'play', + viewBox: '0 0 24 24', + paths: ['M8 5v14l11-7z'] + }, + pause: { + name: 'pause', + viewBox: '0 0 24 24', + paths: ['M6 19h4V5H6v14zm8-14v14h4V5h-4z'] + }, + stop: { + name: 'stop', + viewBox: '0 0 24 24', + paths: ['M6 6h12v12H6z'] + }, + volumeUp: { + name: 'volume-up', + viewBox: '0 0 24 24', + paths: [ + 'M3 9v6h4l5 5V4L7 9H3z', + 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z', + 'M14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z' + ] + }, + volumeOff: { + name: 'volume-off', + viewBox: '0 0 24 24', + paths: [ + 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63z', + 'M19 12c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71z', + 'M4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3z', + 'M12 4L9.91 6.09 12 8.18V4z' + ] + }, + fullscreen: { + name: 'fullscreen', + viewBox: '0 0 24 24', + paths: ['M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'] + }, + exitFullscreen: { + name: 'exit-fullscreen', + viewBox: '0 0 24 24', + paths: ['M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z'] + } +}; + +export function getIcon(name: string): IconDefinition | undefined { + return ICON_DEFINITIONS[name]; +} + +export function getAllIcons(): IconDefinition[] { + return Object.values(ICON_DEFINITIONS); +} + +export function createSVGString(icon: IconDefinition): string { + const pathElements = icon.paths.map(path => ``).join(''); + return `${pathElements}`; +} \ No newline at end of file diff --git a/packages/core/icons/tsconfig.json b/packages/core/icons/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/core/icons/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/core/media-store/package.json b/packages/core/media-store/package.json new file mode 100644 index 00000000..acd5f042 --- /dev/null +++ b/packages/core/media-store/package.json @@ -0,0 +1,29 @@ +{ + "name": "@vjs-10/media-store", + "version": "0.1.0", + "description": "A state abstraction for media. Accepts state owners like a media state owner for sinks and sources of media state.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "state", + "video", + "audio", + "player" + ], + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/core/media-store/src/index.ts b/packages/core/media-store/src/index.ts new file mode 100644 index 00000000..ac059bbc --- /dev/null +++ b/packages/core/media-store/src/index.ts @@ -0,0 +1,46 @@ +export interface MediaState { + currentTime: number; + duration: number; + paused: boolean; + volume: number; + muted: boolean; +} + +export interface MediaStateOwner { + getState(): MediaState; + setState(state: Partial): void; +} + +export class MediaStore { + private owners: Set = new Set(); + private state: MediaState = { + currentTime: 0, + duration: 0, + paused: true, + volume: 1, + muted: false, + }; + + addOwner(owner: MediaStateOwner): void { + this.owners.add(owner); + } + + removeOwner(owner: MediaStateOwner): void { + this.owners.delete(owner); + } + + getState(): MediaState { + return { ...this.state }; + } + + updateState(updates: Partial): void { + this.state = { ...this.state, ...updates }; + this.notifyOwners(); + } + + private notifyOwners(): void { + this.owners.forEach(owner => { + owner.setState(this.state); + }); + } +} \ No newline at end of file diff --git a/packages/core/media-store/tsconfig.json b/packages/core/media-store/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/core/media-store/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/core/media/package.json b/packages/core/media/package.json new file mode 100644 index 00000000..e5300830 --- /dev/null +++ b/packages/core/media/package.json @@ -0,0 +1,29 @@ +{ + "name": "@vjs-10/media", + "version": "0.1.0", + "description": "JS contracts and convenience code for conforming to HTMLMediaElement and related interfaces.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "HTMLMediaElement", + "video", + "audio", + "contracts" + ], + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/core/media/src/index.ts b/packages/core/media/src/index.ts new file mode 100644 index 00000000..15b23992 --- /dev/null +++ b/packages/core/media/src/index.ts @@ -0,0 +1,69 @@ +export type MediaReadyState = 0 | 1 | 2 | 3 | 4; + +export const READY_STATE = { + HAVE_NOTHING: 0 as MediaReadyState, + HAVE_METADATA: 1 as MediaReadyState, + HAVE_CURRENT_DATA: 2 as MediaReadyState, + HAVE_FUTURE_DATA: 3 as MediaReadyState, + HAVE_ENOUGH_DATA: 4 as MediaReadyState, +} as const; + +export type MediaNetworkState = 0 | 1 | 2 | 3; + +export const NETWORK_STATE = { + EMPTY: 0 as MediaNetworkState, + IDLE: 1 as MediaNetworkState, + LOADING: 2 as MediaNetworkState, + NO_SOURCE: 3 as MediaNetworkState, +} as const; + +export interface MediaElementLike { + currentTime: number; + duration: number; + paused: boolean; + ended: boolean; + volume: number; + muted: boolean; + playbackRate: number; + readyState: MediaReadyState; + networkState: MediaNetworkState; + + play(): Promise; + pause(): void; + load(): void; +} + +export function isMediaElement(element: any): element is HTMLMediaElement { + return element && + typeof element.play === 'function' && + typeof element.pause === 'function' && + typeof element.load === 'function' && + typeof element.currentTime === 'number'; +} + +export function createMediaElementAdapter(element: HTMLMediaElement): MediaElementLike { + return { + get currentTime() { return element.currentTime; }, + set currentTime(value: number) { element.currentTime = value; }, + + get duration() { return element.duration; }, + get paused() { return element.paused; }, + get ended() { return element.ended; }, + + get volume() { return element.volume; }, + set volume(value: number) { element.volume = value; }, + + get muted() { return element.muted; }, + set muted(value: boolean) { element.muted = value; }, + + get playbackRate() { return element.playbackRate; }, + set playbackRate(value: number) { element.playbackRate = value; }, + + get readyState() { return element.readyState as MediaReadyState; }, + get networkState() { return element.networkState as MediaNetworkState; }, + + play: () => element.play(), + pause: () => element.pause(), + load: () => element.load(), + }; +} \ No newline at end of file diff --git a/packages/core/media/tsconfig.json b/packages/core/media/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/core/media/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/core/playback-engine/package.json b/packages/core/playback-engine/package.json new file mode 100644 index 00000000..33bc0c8f --- /dev/null +++ b/packages/core/playback-engine/package.json @@ -0,0 +1,30 @@ +{ + "name": "@vjs-10/playback-engine", + "version": "0.1.0", + "description": "An abstraction for building or abstracting media source management (provides a common contract for things like hls.js or dash.js).", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "playback", + "engine", + "hls", + "dash", + "streaming" + ], + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/core/playback-engine/src/index.ts b/packages/core/playback-engine/src/index.ts new file mode 100644 index 00000000..8529552c --- /dev/null +++ b/packages/core/playback-engine/src/index.ts @@ -0,0 +1,83 @@ +export interface MediaSource { + src: string; + type: string; +} + +export interface PlaybackEngineOptions { + autoplay?: boolean; + preload?: 'none' | 'metadata' | 'auto'; +} + +export abstract class PlaybackEngine { + protected element: HTMLMediaElement | null = null; + protected options: PlaybackEngineOptions; + + constructor(options: PlaybackEngineOptions = {}) { + this.options = options; + } + + abstract attach(element: HTMLMediaElement): void; + abstract detach(): void; + abstract load(source: MediaSource): Promise; + abstract play(): Promise; + abstract pause(): void; + abstract seekTo(time: number): void; + + getElement(): HTMLMediaElement | null { + return this.element; + } +} + +export class NativePlaybackEngine extends PlaybackEngine { + attach(element: HTMLMediaElement): void { + this.element = element; + } + + detach(): void { + this.element = null; + } + + async load(source: MediaSource): Promise { + if (!this.element) { + throw new Error('No media element attached'); + } + + this.element.src = source.src; + await new Promise((resolve, reject) => { + const onLoad = () => { + this.element!.removeEventListener('loadedmetadata', onLoad); + this.element!.removeEventListener('error', onError); + resolve(void 0); + }; + const onError = () => { + this.element!.removeEventListener('loadedmetadata', onLoad); + this.element!.removeEventListener('error', onError); + reject(new Error('Failed to load media')); + }; + + this.element.addEventListener('loadedmetadata', onLoad); + this.element.addEventListener('error', onError); + }); + } + + async play(): Promise { + if (!this.element) { + throw new Error('No media element attached'); + } + await this.element.play(); + } + + pause(): void { + if (!this.element) { + throw new Error('No media element attached'); + } + this.element.pause(); + } + + seekTo(time: number): void { + if (!this.element) { + throw new Error('No media element attached'); + } + this.element.currentTime = time; + } +} \ No newline at end of file diff --git a/packages/core/playback-engine/tsconfig.json b/packages/core/playback-engine/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/core/playback-engine/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/html/.gitignore b/packages/html/.gitignore new file mode 100644 index 00000000..2e4362c7 --- /dev/null +++ b/packages/html/.gitignore @@ -0,0 +1,29 @@ +# Web/HTML specific ignores + +# Webpack +.webpack/ + +# Parcel +.parcel-cache/ +.cache/ + +# Vite +.vite/ + +# Bundle analyzer +bundle-analyzer-report.html + +# Service worker +sw.js +sw.js.map + +# Static assets that might be generated +*.generated.js +*.generated.css + +# Web Components polyfills (if downloaded) +webcomponents-*.js + +# PWA related +manifest.json.generated +workbox-*.js \ No newline at end of file diff --git a/packages/html/html-icons/package.json b/packages/html/html-icons/package.json new file mode 100644 index 00000000..cbd9d52c --- /dev/null +++ b/packages/html/html-icons/package.json @@ -0,0 +1,33 @@ +{ + "name": "@vjs-10/html-icons", + "version": "0.1.0", + "description": "HTML/DOM-specific icon components derived from @vjs-10/icons", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "icons", + "html", + "dom", + "web-components", + "media", + "player" + ], + "license": "Apache-2.0", + "dependencies": { + "@vjs-10/icons": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/html/html-icons/src/index.ts b/packages/html/html-icons/src/index.ts new file mode 100644 index 00000000..334c8784 --- /dev/null +++ b/packages/html/html-icons/src/index.ts @@ -0,0 +1,134 @@ +import { getIcon, createSVGString, IconDefinition } from '@vjs-10/icons'; + +export interface IconElementOptions { + className?: string; + size?: number; + color?: string; +} + +export function createIconElement(iconName: string, options: IconElementOptions = {}): HTMLElement { + const icon = getIcon(iconName); + if (!icon) { + throw new Error(`Icon "${iconName}" not found`); + } + + const container = document.createElement('span'); + container.className = `vjs-icon vjs-icon-${iconName} ${options.className || ''}`.trim(); + + const svgString = createSVGString(icon); + container.innerHTML = svgString; + + const svg = container.querySelector('svg'); + if (svg && options.size) { + svg.style.width = `${options.size}px`; + svg.style.height = `${options.size}px`; + } + + if (svg && options.color) { + svg.style.fill = options.color; + } + + return container; +} + +export class VjsIcon extends HTMLElement { + private _name: string = ''; + private _size?: number; + private _color?: string; + + static get observedAttributes() { + return ['name', 'size', 'color']; + } + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + if (oldValue !== newValue) { + switch (name) { + case 'name': + this._name = newValue; + break; + case 'size': + this._size = newValue ? parseInt(newValue, 10) : undefined; + break; + case 'color': + this._color = newValue; + break; + } + this.render(); + } + } + + private render() { + if (!this.shadowRoot || !this._name) return; + + const icon = getIcon(this._name); + if (!icon) { + this.shadowRoot.innerHTML = `Icon "${this._name}" not found`; + return; + } + + const svgString = createSVGString(icon); + const styles = ` + + `; + + this.shadowRoot.innerHTML = styles + svgString; + } + + get name() { + return this._name; + } + + set name(value: string) { + this.setAttribute('name', value); + } + + get size() { + return this._size; + } + + set size(value: number | undefined) { + if (value !== undefined) { + this.setAttribute('size', value.toString()); + } else { + this.removeAttribute('size'); + } + } + + get color() { + return this._color; + } + + set color(value: string | undefined) { + if (value !== undefined) { + this.setAttribute('color', value); + } else { + this.removeAttribute('color'); + } + } +} + +if (!customElements.get('vjs-icon')) { + customElements.define('vjs-icon', VjsIcon); +} + +export { getIcon, createSVGString } from '@vjs-10/icons'; \ No newline at end of file diff --git a/packages/html/html-icons/tsconfig.json b/packages/html/html-icons/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/html/html-icons/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/html/html-media-elements/package.json b/packages/html/html-media-elements/package.json new file mode 100644 index 00000000..272863e8 --- /dev/null +++ b/packages/html/html-media-elements/package.json @@ -0,0 +1,34 @@ +{ + "name": "@vjs-10/html-media-elements", + "version": "0.1.0", + "description": "HTML/DOM-specific media element components and utilities", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "html", + "dom", + "web-components", + "video", + "audio" + ], + "license": "Apache-2.0", + "dependencies": { + "@vjs-10/media": "workspace:*", + "@vjs-10/playback-engine": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/html/html-media-elements/src/index.ts b/packages/html/html-media-elements/src/index.ts new file mode 100644 index 00000000..31604c47 --- /dev/null +++ b/packages/html/html-media-elements/src/index.ts @@ -0,0 +1,155 @@ +import { MediaElementLike, createMediaElementAdapter } from '@vjs-10/media'; +import { PlaybackEngine, NativePlaybackEngine } from '@vjs-10/playback-engine'; + +export interface MediaElementOptions { + playbackEngine?: PlaybackEngine; + controls?: boolean; + autoplay?: boolean; + preload?: 'none' | 'metadata' | 'auto'; +} + +export class VjsMediaElement extends HTMLElement { + private mediaElement: HTMLVideoElement | HTMLAudioElement; + private playbackEngine: PlaybackEngine; + private adapter: MediaElementLike; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + const mediaType = this.getAttribute('media-type') || 'video'; + this.mediaElement = mediaType === 'audio' + ? document.createElement('audio') + : document.createElement('video'); + + this.playbackEngine = new NativePlaybackEngine(); + this.adapter = createMediaElementAdapter(this.mediaElement); + } + + static get observedAttributes() { + return ['src', 'controls', 'autoplay', 'preload', 'media-type']; + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + this.playbackEngine.attach(this.mediaElement); + } + + disconnectedCallback() { + this.playbackEngine.detach(); + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + if (oldValue === newValue) return; + + switch (name) { + case 'src': + if (newValue) { + this.playbackEngine.load({ src: newValue, type: 'video/mp4' }); + } + break; + case 'controls': + this.mediaElement.controls = newValue !== null; + break; + case 'autoplay': + this.mediaElement.autoplay = newValue !== null; + break; + case 'preload': + this.mediaElement.preload = newValue as any; + break; + } + } + + private render() { + if (!this.shadowRoot) return; + + const styles = ` + + `; + + this.shadowRoot.innerHTML = styles; + this.shadowRoot.appendChild(this.mediaElement); + } + + private setupEventListeners() { + this.mediaElement.addEventListener('play', () => { + this.dispatchEvent(new CustomEvent('vjs-play')); + }); + + this.mediaElement.addEventListener('pause', () => { + this.dispatchEvent(new CustomEvent('vjs-pause')); + }); + + this.mediaElement.addEventListener('timeupdate', () => { + this.dispatchEvent(new CustomEvent('vjs-timeupdate', { + detail: { currentTime: this.mediaElement.currentTime } + })); + }); + + this.mediaElement.addEventListener('loadedmetadata', () => { + this.dispatchEvent(new CustomEvent('vjs-loadedmetadata', { + detail: { duration: this.mediaElement.duration } + })); + }); + } + + async play() { + return this.playbackEngine.play(); + } + + pause() { + this.playbackEngine.pause(); + } + + seekTo(time: number) { + this.playbackEngine.seekTo(time); + } + + get currentTime() { + return this.adapter.currentTime; + } + + set currentTime(value: number) { + this.adapter.currentTime = value; + } + + get duration() { + return this.adapter.duration; + } + + get paused() { + return this.adapter.paused; + } + + get volume() { + return this.adapter.volume; + } + + set volume(value: number) { + this.adapter.volume = value; + } + + get muted() { + return this.adapter.muted; + } + + set muted(value: boolean) { + this.adapter.muted = value; + } +} + +if (!customElements.get('vjs-media')) { + customElements.define('vjs-media', VjsMediaElement); +} + +export { MediaElementLike, createMediaElementAdapter } from '@vjs-10/media'; \ No newline at end of file diff --git a/packages/html/html-media-elements/tsconfig.json b/packages/html/html-media-elements/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/html/html-media-elements/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/html/html-media-store/package.json b/packages/html/html-media-store/package.json new file mode 100644 index 00000000..c03186f6 --- /dev/null +++ b/packages/html/html-media-store/package.json @@ -0,0 +1,33 @@ +{ + "name": "@vjs-10/html-media-store", + "version": "0.1.0", + "description": "HTML/DOM-specific media store integration and components", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "store", + "state", + "html", + "dom" + ], + "license": "Apache-2.0", + "dependencies": { + "@vjs-10/media-store": "workspace:*", + "@vjs-10/html-media-elements": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/html/html-media-store/src/index.ts b/packages/html/html-media-store/src/index.ts new file mode 100644 index 00000000..0f27d492 --- /dev/null +++ b/packages/html/html-media-store/src/index.ts @@ -0,0 +1,84 @@ +import { MediaStore, MediaState, MediaStateOwner } from '@vjs-10/media-store'; +import { VjsMediaElement } from '@vjs-10/html-media-elements'; + +export class HTMLMediaStateOwner implements MediaStateOwner { + private element: VjsMediaElement; + private listeners: Map = new Map(); + + constructor(element: VjsMediaElement) { + this.element = element; + this.setupEventListeners(); + } + + getState(): MediaState { + return { + currentTime: this.element.currentTime, + duration: this.element.duration, + paused: this.element.paused, + volume: this.element.volume, + muted: this.element.muted, + }; + } + + setState(state: Partial): void { + if (state.currentTime !== undefined && state.currentTime !== this.element.currentTime) { + this.element.currentTime = state.currentTime; + } + if (state.volume !== undefined && state.volume !== this.element.volume) { + this.element.volume = state.volume; + } + if (state.muted !== undefined && state.muted !== this.element.muted) { + this.element.muted = state.muted; + } + } + + private setupEventListeners() { + const timeUpdateListener = () => { + this.dispatchStateChange(); + }; + + const playPauseListener = () => { + this.dispatchStateChange(); + }; + + const volumeChangeListener = () => { + this.dispatchStateChange(); + }; + + this.element.addEventListener('vjs-timeupdate', timeUpdateListener); + this.element.addEventListener('vjs-play', playPauseListener); + this.element.addEventListener('vjs-pause', playPauseListener); + this.element.addEventListener('volumechange', volumeChangeListener); + + this.listeners.set('timeupdate', timeUpdateListener); + this.listeners.set('play', playPauseListener); + this.listeners.set('pause', playPauseListener); + this.listeners.set('volumechange', volumeChangeListener); + } + + private dispatchStateChange() { + this.element.dispatchEvent(new CustomEvent('vjs-state-change', { + detail: this.getState() + })); + } + + destroy() { + this.listeners.forEach((listener, event) => { + this.element.removeEventListener(event as any, listener); + }); + this.listeners.clear(); + } +} + +export function connectMediaElementToStore(element: VjsMediaElement, store: MediaStore): HTMLMediaStateOwner { + const owner = new HTMLMediaStateOwner(element); + store.addOwner(owner); + + element.addEventListener('vjs-state-change', (event: CustomEvent) => { + store.updateState(event.detail); + }); + + return owner; +} + +export { MediaStore, MediaState, MediaStateOwner } from '@vjs-10/media-store'; \ No newline at end of file diff --git a/packages/html/html-media-store/tsconfig.json b/packages/html/html-media-store/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/html/html-media-store/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/html/html/package.json b/packages/html/html/package.json new file mode 100644 index 00000000..5f802432 --- /dev/null +++ b/packages/html/html/package.json @@ -0,0 +1,34 @@ +{ + "name": "@vjs-10/html", + "version": "0.1.0", + "description": "Core UI components for building media player UIs. Also an import + export of various other HTML packages for convenience and discoverability.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "player", + "html", + "ui", + "components" + ], + "license": "Apache-2.0", + "dependencies": { + "@vjs-10/html-icons": "workspace:*", + "@vjs-10/html-media-elements": "workspace:*", + "@vjs-10/html-media-store": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/html/html/src/index.ts b/packages/html/html/src/index.ts new file mode 100644 index 00000000..0b337b0b --- /dev/null +++ b/packages/html/html/src/index.ts @@ -0,0 +1,216 @@ +export * from '@vjs-10/html-icons'; +export * from '@vjs-10/html-media-elements'; +export * from '@vjs-10/html-media-store'; + +export interface PlayerOptions { + controls?: boolean; + autoplay?: boolean; + preload?: 'none' | 'metadata' | 'auto'; + width?: number; + height?: number; +} + +export class VjsPlayer extends HTMLElement { + private mediaElement: HTMLElement; + private controlBar: HTMLElement; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + this.mediaElement = document.createElement('vjs-media'); + this.controlBar = this.createControlBar(); + } + + static get observedAttributes() { + return ['src', 'controls', 'autoplay', 'preload', 'width', 'height']; + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + if (oldValue === newValue) return; + + switch (name) { + case 'src': + case 'controls': + case 'autoplay': + case 'preload': + this.mediaElement.setAttribute(name, newValue); + break; + case 'width': + case 'height': + this.updateDimensions(); + break; + } + } + + private render() { + if (!this.shadowRoot) return; + + const styles = ` + + `; + + const template = ` +
+ ${this.mediaElement.outerHTML} + ${this.getAttribute('controls') !== null ? this.controlBar.outerHTML : ''} +
+ `; + + this.shadowRoot.innerHTML = styles + template; + + const mediaEl = this.shadowRoot.querySelector('vjs-media'); + const controlBarEl = this.shadowRoot.querySelector('.vjs-control-bar'); + + if (mediaEl) { + this.mediaElement = mediaEl as HTMLElement; + } + if (controlBarEl) { + this.controlBar = controlBarEl as HTMLElement; + } + } + + private createControlBar(): HTMLElement { + const controlBar = document.createElement('div'); + controlBar.className = 'vjs-control-bar'; + + controlBar.innerHTML = ` + +
+
+
+ + `; + + return controlBar; + } + + private setupEventListeners() { + const playButton = this.shadowRoot?.querySelector('.vjs-play-button'); + const progressBar = this.shadowRoot?.querySelector('.vjs-progress-bar'); + const volumeButton = this.shadowRoot?.querySelector('.vjs-volume-button'); + + playButton?.addEventListener('click', () => { + if (this.mediaElement && 'paused' in this.mediaElement) { + if ((this.mediaElement as any).paused) { + (this.mediaElement as any).play(); + } else { + (this.mediaElement as any).pause(); + } + } + }); + + this.mediaElement?.addEventListener('vjs-play', () => { + const icon = playButton?.querySelector('vjs-icon'); + if (icon) { + (icon as any).name = 'pause'; + } + }); + + this.mediaElement?.addEventListener('vjs-pause', () => { + const icon = playButton?.querySelector('vjs-icon'); + if (icon) { + (icon as any).name = 'play'; + } + }); + + this.mediaElement?.addEventListener('vjs-timeupdate', (event: CustomEvent) => { + const progressFill = this.shadowRoot?.querySelector('.vjs-progress-fill') as HTMLElement; + if (progressFill && this.mediaElement && 'duration' in this.mediaElement) { + const duration = (this.mediaElement as any).duration; + const currentTime = event.detail.currentTime; + const percentage = duration ? (currentTime / duration) * 100 : 0; + progressFill.style.width = `${percentage}%`; + } + }); + } + + private updateDimensions() { + const width = this.getAttribute('width'); + const height = this.getAttribute('height'); + + if (width) { + this.style.width = width.includes('px') ? width : `${width}px`; + } + if (height) { + this.style.height = height.includes('px') ? height : `${height}px`; + } + } +} + +if (!customElements.get('vjs-player')) { + customElements.define('vjs-player', VjsPlayer); +} \ No newline at end of file diff --git a/packages/html/html/tsconfig.json b/packages/html/html/tsconfig.json new file mode 100644 index 00000000..c419f915 --- /dev/null +++ b/packages/html/html/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/react-native/.gitignore b/packages/react-native/.gitignore new file mode 100644 index 00000000..1a5db810 --- /dev/null +++ b/packages/react-native/.gitignore @@ -0,0 +1,52 @@ +# React Native specific ignores + +# Metro bundler +.metro-health-check* + +# React Native Debugger +react-native-debugger.log + +# Flipper +Flipper/ + +# Bundle artifacts +*.jsbundle + +# Android specific +android/app/build/ +android/app/src/main/assets/ +android/.gradle/ +android/build/ +android/gradle/ +android/gradlew* +android/local.properties +android/app/release/ + +# iOS specific +ios/build/ +ios/Pods/ +ios/*.xcworkspace +ios/*.xcodeproj/xcuserdata/ +ios/*.xcodeproj/xcshareddata/ +ios/Podfile.lock +ios/DerivedData/ + +# CocoaPods +Podfile.lock +Pods/ + +# Fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +# Detox +.detoxrc.json +detox_tests/ + +# react-native-codegen +build/generated/ + +# Ruby (for iOS development) +vendor/bundle/ \ No newline at end of file diff --git a/packages/react-native/react-native-icons/package.json b/packages/react-native/react-native-icons/package.json new file mode 100644 index 00000000..61f2500a --- /dev/null +++ b/packages/react-native/react-native-icons/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vjs-10/react-native-icons", + "version": "0.1.0", + "description": "React Native-specific icon components derived from @vjs-10/icons", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "icons", + "react-native", + "media", + "player", + "mobile" + ], + "license": "Apache-2.0", + "dependencies": { + "@vjs-10/icons": "workspace:*" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-native": ">=0.60.0", + "react-native-svg": ">=12.0.0" + }, + "devDependencies": { + "typescript": "^5.3.0", + "@types/react": "^18.0.0", + "@types/react-native": "^0.72.0", + "react": "^18.0.0", + "react-native": "^0.72.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/react-native/react-native-icons/src/index.tsx b/packages/react-native/react-native-icons/src/index.tsx new file mode 100644 index 00000000..10c86196 --- /dev/null +++ b/packages/react-native/react-native-icons/src/index.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { getIcon } from '@vjs-10/icons'; +import Svg, { Path, SvgProps } from 'react-native-svg'; + +export interface IconProps extends Omit { + name: string; + size?: number; + color?: string; +} + +export const Icon: React.FC = ({ + name, + size = 24, + color = '#000000', + ...props +}) => { + const icon = getIcon(name); + + if (!icon) { + console.warn(`Icon "${name}" not found`); + return null; + } + + return ( + + {icon.paths.map((path, index) => ( + + ))} + + ); +}; + +export const PlayIcon: React.FC> = (props) => ( + +); + +export const PauseIcon: React.FC> = (props) => ( + +); + +export const StopIcon: React.FC> = (props) => ( + +); + +export const VolumeUpIcon: React.FC> = (props) => ( + +); + +export const VolumeOffIcon: React.FC> = (props) => ( + +); + +export const FullscreenIcon: React.FC> = (props) => ( + +); + +export const ExitFullscreenIcon: React.FC> = (props) => ( + +); + +export { getIcon, getAllIcons, createSVGString } from '@vjs-10/icons'; \ No newline at end of file diff --git a/packages/react-native/react-native-icons/tsconfig.json b/packages/react-native/react-native-icons/tsconfig.json new file mode 100644 index 00000000..53e48dba --- /dev/null +++ b/packages/react-native/react-native-icons/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} \ No newline at end of file diff --git a/packages/react-native/react-native-media-elements/package.json b/packages/react-native/react-native-media-elements/package.json new file mode 100644 index 00000000..0320b189 --- /dev/null +++ b/packages/react-native/react-native-media-elements/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vjs-10/react-native-media-elements", + "version": "0.1.0", + "description": "React Native-specific media element components and hooks", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\"", + "clean": "rm -rf dist" + }, + "keywords": [ + "media", + "react-native", + "video", + "audio", + "mobile" + ], + "license": "Apache-2.0", + "dependencies": { + "@vjs-10/media": "workspace:*" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-native": ">=0.60.0", + "react-native-video": ">=5.0.0" + }, + "devDependencies": { + "typescript": "^5.3.0", + "@types/react": "^18.0.0", + "@types/react-native": "^0.72.0", + "react": "^18.0.0", + "react-native": "^0.72.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/react-native/react-native-media-elements/src/index.tsx b/packages/react-native/react-native-media-elements/src/index.tsx new file mode 100644 index 00000000..f46f468c --- /dev/null +++ b/packages/react-native/react-native-media-elements/src/index.tsx @@ -0,0 +1,181 @@ +import React, { useRef, useImperativeHandle, forwardRef } from 'react'; +import { StyleSheet, ViewStyle } from 'react-native'; +import Video, { VideoRef, OnLoadData, OnProgressData } from 'react-native-video'; +import { MediaReadyState, MediaNetworkState, READY_STATE, NETWORK_STATE } from '@vjs-10/media'; + +export interface MediaElementProps { + source?: { uri: string }; + controls?: boolean; + paused?: boolean; + muted?: boolean; + volume?: number; + rate?: number; + repeat?: boolean; + resizeMode?: 'contain' | 'cover' | 'stretch'; + onLoad?: (data: OnLoadData) => void; + onProgress?: (data: OnProgressData) => void; + onEnd?: () => void; + onError?: (error: any) => void; + style?: ViewStyle; +} + +export interface MediaElementRef { + currentTime: number; + duration: number; + paused: boolean; + ended: boolean; + volume: number; + muted: boolean; + playbackRate: number; + readyState: MediaReadyState; + networkState: MediaNetworkState; + + play(): Promise; + pause(): void; + load(): void; + seek(time: number): void; +} + +export const VideoElement = forwardRef( + ({ + source, + controls = false, + paused = true, + muted = false, + volume = 1.0, + rate = 1.0, + repeat = false, + resizeMode = 'contain', + onLoad, + onProgress, + onEnd, + onError, + style, + }, ref) => { + const videoRef = useRef(null); + const stateRef = useRef({ + currentTime: 0, + duration: 0, + paused: true, + ended: false, + volume: 1.0, + muted: false, + playbackRate: 1.0, + readyState: READY_STATE.HAVE_NOTHING, + networkState: NETWORK_STATE.EMPTY, + }); + + const handleLoad = (data: OnLoadData) => { + stateRef.current.duration = data.duration; + stateRef.current.readyState = READY_STATE.HAVE_METADATA; + stateRef.current.networkState = NETWORK_STATE.IDLE; + onLoad?.(data); + }; + + const handleProgress = (data: OnProgressData) => { + stateRef.current.currentTime = data.currentTime; + onProgress?.(data); + }; + + const handleEnd = () => { + stateRef.current.ended = true; + stateRef.current.paused = true; + onEnd?.(); + }; + + const handleError = (error: any) => { + stateRef.current.networkState = NETWORK_STATE.NO_SOURCE; + onError?.(error); + }; + + useImperativeHandle(ref, () => ({ + get currentTime() { + return stateRef.current.currentTime; + }, + get duration() { + return stateRef.current.duration; + }, + get paused() { + return stateRef.current.paused; + }, + get ended() { + return stateRef.current.ended; + }, + get volume() { + return stateRef.current.volume; + }, + get muted() { + return stateRef.current.muted; + }, + get playbackRate() { + return stateRef.current.playbackRate; + }, + get readyState() { + return stateRef.current.readyState; + }, + get networkState() { + return stateRef.current.networkState; + }, + play: async () => { + stateRef.current.paused = false; + stateRef.current.ended = false; + }, + pause: () => { + stateRef.current.paused = true; + }, + load: () => { + stateRef.current.networkState = NETWORK_STATE.LOADING; + }, + seek: (time: number) => { + videoRef.current?.seek(time); + stateRef.current.currentTime = time; + }, + }), []); + + React.useEffect(() => { + stateRef.current.paused = paused; + }, [paused]); + + React.useEffect(() => { + stateRef.current.volume = volume; + }, [volume]); + + React.useEffect(() => { + stateRef.current.muted = muted; + }, [muted]); + + React.useEffect(() => { + stateRef.current.playbackRate = rate; + }, [rate]); + + return ( +