# @hiyve/react-media-player

Audio and video media player with waveform visualization, region management, and playback controls.

## Installation

```bash
npx @hiyve/cli login
npm install @hiyve/react-media-player
```

## Quick Start

```tsx
import { MediaPlayer } from '@hiyve/react-media-player';

function App() {
  return (
    <MediaPlayer
      src="https://example.com/audio.mp3"
      onError={(err) => console.error(err)}
    />
  );
}
```

### With all features

```tsx
import { MediaPlayer } from '@hiyve/react-media-player';

<MediaPlayer
  src={audioUrl}
  mediaType="audio"
  enableRegions
  regions={savedRegions}
  onRegionChange={(region) => saveRegion(region)}
  onRegionDelete={(id) => deleteRegion(id)}
  colors={{
    waveColor: '#6366f1',
    progressColor: '#ec4899',
  }}
/>
```

### Custom UI with hooks

```tsx
import { useMediaPlayer } from '@hiyve/react-media-player';

function CustomPlayer({ src }: { src: string }) {
  const player = useMediaPlayer({ src });

  return (
    <div>
      <button onClick={player.togglePlayPause}>
        {player.isPlaying ? 'Pause' : 'Play'}
      </button>
      <span>{player.playbackTime}s / {player.playbackLength}s</span>
    </div>
  );
}
```

## Features

- Audio and video playback with adaptive media type detection
- Waveform visualization (auto-skipped for files >200 MB)
- Play/pause, rewind, seek, and mute controls
- Volume and playback rate adjustment (0.5x–2.0x) via popup sliders
- Loop region creation via drag selection on the waveform
- Named region creation, editing, and persistence — click a region to edit it, double-click for the naming dialog
- Audio passthrough for sharing playback in Hiyve meetings
- Keyboard shortcuts (space for play/pause)
- Full label and color customization
- Safari and iOS compatibility
- Compact layout mode for constrained spaces

## MediaPlayer Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `src` | `string` | *required* | Media URL to play |
| `mediaType` | `'audio' \| 'video'` | auto-detected | Force media type instead of auto-detecting from URL |
| `fileSize` | `number` | — | File size in bytes; files >200 MB skip waveform |
| `enableWaveform` | `boolean` | `true` | Show waveform visualization |
| `enableVolumeControl` | `boolean` | `true` | Show volume/gain control button |
| `enableRateControl` | `boolean` | `true` | Show playback rate control button (auto-disabled on Safari) |
| `enableRegions` | `boolean` | `false` | Enable region creation, editing, and loop playback |
| `enableAudioPassthrough` | `boolean` | `false` | Enable audio passthrough to a Hiyve meeting |
| `enableKeyboardShortcuts` | `boolean` | `true` | Enable keyboard shortcuts (space for play/pause) |
| `enableMute` | `boolean` | `true` | Show mute/unmute button |
| `compact` | `boolean` | `false` | Compact layout mode |
| `regions` | `RegionData[]` | — | Initial named regions to load |
| `onRegionChange` | `(region: RegionData) => void` | — | Called when a named region is created or updated |
| `onRegionDelete` | `(regionId: string) => void` | — | Called when a named region is deleted |
| `externalAudioSource` | `ExternalAudioSource` | — | External audio source for gain visualization in the mixer |
| `labels` | `Partial<MediaPlayerLabels>` | — | Override default labels/tooltips |
| `colors` | `Partial<MediaPlayerColors>` | — | Override default colors |
| `onPlay` | `() => void` | — | Called when playback starts |
| `onPause` | `() => void` | — | Called when playback pauses |
| `onTimeUpdate` | `(time: number) => void` | — | Called on playback time change |
| `onEnded` | `() => void` | — | Called when playback ends |
| `onError` | `(error: Error) => void` | — | Called on errors (fetch, waveform, playback) |
| `onAudioStreamReady` | `(stream: MediaStream) => void` | — | Called when the audio output stream is ready |

## Sub-components

These are the building blocks used by `MediaPlayer`. They are exported for advanced use cases where you need a custom layout.

| Component | Description |
|-----------|-------------|
| `PlaybackControls` | Toolbar with play/pause, rewind, mute, loop, region, passthrough, volume, and rate buttons |
| `TimeSlider` | Horizontal slider showing current time and duration |
| `Waveform` | Container element for the WaveSurfer waveform visualization |
| `VolumeSlider` | Vertical slider with optional click-point snapping |
| `GainMixer` | Volume and external audio gain controls with visualizer |
| `GainMixerPopup` | Popup wrapper for `GainMixer` |
| `RateMixer` | Playback rate slider with discrete rate steps (0.5x–2.0x) |
| `RateMixerPopup` | Popup wrapper for `RateMixer` |
| `GainAnalyser` | Connects to a gain/analyser node and feeds data to `GainVisualizer` |
| `GainVisualizer` | Canvas-based audio level meter with peak indicators |
| `NamedRegionDialog` | Dialog for naming, saving, or deleting a named region |

## Hooks

### `useMediaPlayer(options)`

Core playback hook that manages the media element, Web Audio API, and playback state.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `src` | `string` | yes | Media URL |
| `mediaType` | `'audio' \| 'video'` | no | Force media type |
| `fileSize` | `number` | no | File size in bytes |
| `onError` | `(error: Error) => void` | no | Error callback |
| `onAudioStreamReady` | `(stream: MediaStream) => void` | no | Stream ready callback |
| `onPlay` | `() => void` | no | Play callback |
| `onPause` | `() => void` | no | Pause callback |
| `onTimeUpdate` | `(time: number) => void` | no | Time update callback |
| `onEnded` | `() => void` | no | Ended callback |

**Returns** (`UseMediaPlayerResult`):

| Property | Type | Description |
|----------|------|-------------|
| `mediaRef` | `RefObject<HTMLMediaElement>` | Ref to the media element |
| `audioContextRef` | `RefObject<AudioContext>` | Ref to the audio context |
| `gainNodeRef` | `RefObject<GainNode>` | Ref to the gain node |
| `destinationStreamRef` | `RefObject<MediaStreamAudioDestinationNode>` | Ref to the destination stream |
| `playerId` | `string` | Unique player instance ID |
| `isPlaying` | `boolean` | Whether media is playing |
| `loaded` | `boolean` | Whether media is loaded and ready |
| `playbackTime` | `number` | Current playback time in seconds |
| `playbackLength` | `number` | Total duration in seconds |
| `isMuted` | `boolean` | Whether audio is muted |
| `playbackRate` | `number` | Current playback rate |
| `detectedMediaType` | `'audio' \| 'video' \| null` | Detected media type |
| `skipWaveform` | `boolean` | Whether waveform was skipped (large file) |
| `disableWebAudio` | `boolean` | Whether Web Audio API is disabled (iOS) |
| `play` | `() => void` | Start playback |
| `pause` | `() => void` | Pause playback |
| `togglePlayPause` | `() => void` | Toggle play/pause |
| `rewind` | `() => void` | Rewind to start |
| `seek` | `(time: number) => void` | Seek to time |
| `setRate` | `(rate: number) => void` | Set playback rate |
| `toggleMute` | `() => void` | Toggle mute |

### `useWaveform(options)`

Creates a WaveSurfer waveform visualization for a media element.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `containerId` | `string` | yes | DOM element ID for the waveform container |
| `mediaElement` | `HTMLMediaElement \| null` | yes | The media element to visualize |
| `enabled` | `boolean` | yes | Whether to create the waveform |
| `colors` | `MediaPlayerColors` | yes | Color configuration |
| `onReady` | `(duration: number) => void` | no | Called when waveform is ready |
| `onTimeUpdate` | `(time: number) => void` | no | Called on time update |
| `onError` | `(error: Error) => void` | no | Error callback |

**Returns** (`UseWaveformResult`):

| Property | Type | Description |
|----------|------|-------------|
| `wavesurfer` | `WaveSurfer \| null` | WaveSurfer instance |
| `regionsPlugin` | `RegionsPlugin \| null` | Regions plugin instance |
| `isReady` | `boolean` | Whether waveform is ready |

### `useRegions(options)`

Manages region editing and loop playback on the waveform. Toggle edit mode to create, rename, move, resize, or delete regions. Outside edit mode, click a region to loop-play it.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `regionsPlugin` | `RegionsPlugin \| null` | yes | Regions plugin from `useWaveform` |
| `initialRegions` | `RegionData[]` | no | Initial regions to load |
| `onRegionChange` | `(region: RegionData) => void` | no | Called when a region is created or updated |
| `onRegionDelete` | `(regionId: string) => void` | no | Called when a region is deleted |
| `colors` | `MediaPlayerColors` | yes | Color configuration |
| `setIsPlaying` | `(playing: boolean) => void` | yes | Callback to update playing state when a region starts playback |

**Returns** (`UseRegionsResult`):

| Property | Type | Description |
|----------|------|-------------|
| `isEditMode` | `boolean` | Whether region edit mode is active |
| `openRegionDialog` | `boolean` | Whether the naming dialog is open |
| `activeRegionName` | `string` | Name of the region being edited |
| `handleEditToggle` | `() => void` | Toggle region edit mode |
| `handleSaveNamedRegion` | `(name: string) => void` | Save region name |
| `handleDeleteNamedRegion` | `() => void` | Delete the active region |
| `closeRegionDialog` | `() => void` | Close the naming dialog |

### `useAudioPassthrough(options)`

Manages audio passthrough state for sharing playback audio in a meeting.

When toggled, dispatches a `hiyve-media-passthrough` `CustomEvent` on `window` with `{ stream, active }` in the detail. Listen for this event to wire the stream into your audio pipeline.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `destinationStream` | `MediaStreamAudioDestinationNode \| null` | yes | Destination stream from `useMediaPlayer` |
| `enabled` | `boolean` | yes | Whether passthrough is enabled |
| `onError` | `(error: Error) => void` | no | Error callback |

**Returns** (`UseAudioPassthroughResult`):

| Property | Type | Description |
|----------|------|-------------|
| `isActive` | `boolean` | Whether passthrough is currently active |
| `toggle` | `() => Promise<void>` | Toggle passthrough on/off |

## Types

| Type | Description |
|------|-------------|
| `MediaPlayerProps` | Props for the `MediaPlayer` component |
| `RegionData` | Region data (`id`, `start`, `end`, `content`, optional `channelIdx`) |
| `ExternalAudioSource` | External audio source with optional `analyser` and `gainNode` |
| `MediaPlayerLabels` | All label/tooltip strings (24 properties) |
| `MediaPlayerColors` | All color values (16 properties) |
| `PlaybackControlsProps` | Props for `PlaybackControls` |
| `TimeSliderProps` | Props for `TimeSlider` |
| `WaveformProps` | Props for `Waveform` |
| `VolumeSliderProps` | Props for `VolumeSlider` |
| `VolumeSliderColors` | Color configuration for `VolumeSlider` |
| `GainMixerProps` | Props for `GainMixer` |
| `GainMixerPopupProps` | Props for `GainMixerPopup` |
| `RateMixerProps` | Props for `RateMixer` |
| `RateMixerPopupProps` | Props for `RateMixerPopup` |
| `GainAnalyserProps` | Props for `GainAnalyser` |
| `GainVisualizerProps` | Props for `GainVisualizer` |
| `NamedRegionDialogProps` | Props for `NamedRegionDialog` |
| `UseMediaPlayerOptions` | Options for `useMediaPlayer` |
| `UseMediaPlayerResult` | Return type of `useMediaPlayer` |
| `UseWaveformOptions` | Options for `useWaveform` |
| `UseWaveformResult` | Return type of `useWaveform` |
| `UseRegionsOptions` | Options for `useRegions` |
| `UseRegionsResult` | Return type of `useRegions` |
| `UseAudioPassthroughOptions` | Options for `useAudioPassthrough` |
| `UseAudioPassthroughResult` | Return type of `useAudioPassthrough` |
| `WaveSurferRegion` | WaveSurfer region instance type |

## Customization

Override default labels and colors by passing partial objects — unspecified keys use defaults.

### Labels

```tsx
import { MediaPlayer, defaultMediaPlayerLabels } from '@hiyve/react-media-player';

<MediaPlayer
  src={url}
  labels={{
    play: 'Reproducir',
    pause: 'Pausar',
    rewind: 'Rebobinar',
    mute: 'Silenciar',
    unmute: 'Activar sonido',
  }}
/>
```

### Colors

```tsx
import { MediaPlayer, defaultMediaPlayerColors } from '@hiyve/react-media-player';

<MediaPlayer
  src={url}
  colors={{
    waveColor: '#6366f1',
    progressColor: '#ec4899',
    cursorColor: '#f59e0b',
    controlBackground: '#1e1e2e',
    controlIcon: '#cdd6f4',
  }}
/>
```

**Defaults and merge functions:**

| Default | Merge Function |
|---------|---------------|
| `defaultMediaPlayerLabels` | `mergeMediaPlayerLabels` |
| `defaultMediaPlayerColors` | `mergeMediaPlayerColors` |

## Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `LARGE_FILE_THRESHOLD` | `209715200` (200 MB) | Files above this size skip waveform |
| `FETCH_TIMEOUT_MS` | `60000` | Media fetch timeout in milliseconds |
| `DEFAULT_VOLUME_RANGE` | `[0, 0.05, ..., 1.0]` | Volume slider range values (5% increments) |
| `DEFAULT_VOLUME_CLICK_POINTS` | `[0, 5, ..., 100]` | Volume slider click points |
| `MIC_GAIN_RANGE` | `[0, 0.25, ..., 5.0]` | Mic gain range (0–500% in 25% increments) |
| `RATE_RANGE_VALUES` | `[0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]` | Playback rate steps |
| `RATE_CLICK_POINTS` | `[0, 17, 33, 50, 67, 83, 100]` | Rate slider click points |
| `GAIN_FACTOR` | `30` | Gain visualizer scale factor |
| `PEAK_DECAY_RATE` | `0.01` | Peak indicator decay rate |
| `PEAK_THRESHOLD` | `10` | Minimum peak value for indicator |
| `IS_SAFARI` | `boolean` | Safari browser detection |
| `IS_IOS` | `boolean` | iOS device detection (includes iPadOS 13+) |

## Requirements

- **`@mui/material`** (`^5.0.0 || ^6.0.0`) and **`@mui/icons-material`**
- **`@emotion/react`** (`^11.0.0`) and **`@emotion/styled`** (`^11.0.0`)
- **`react`** (`^18.0.0`)
- **`@hiyve/react`** (`^2.0.0`) — *optional*; only needed for audio passthrough in a Hiyve meeting

## License

MIT
