Skip to main content
Media Player is a sophisticated multimedia application that supports audio, video, and image playback with an audio visualizer, drag-and-drop support, and playlist organization.

Features

  • Multi-Format Support: Audio (MP3, WAV, OGG), Video (MP4, WebM), Images (JPG, PNG, GIF)
  • Audio Visualizer: Real-time frequency spectrum analyzer
  • Playlist Management: Multiple folders, drag-and-drop, reordering
  • Drag & Drop: Drop media files directly into player
  • Volume Control: Independent volume slider with global volume integration
  • Playback Controls: Play, Pause, Stop, Previous, Next, Loop
  • Folder Organization: Organize media into custom folders
  • Scrolling Title: Animated ticker for long track names
  • Responsive Design: Adapts to window size

Component Structure

Location: src/WinXP/apps/MediaPlayer/index.jsx
export default function MediaPlayer() {
  const [library, setLibrary] = useState(() => {
    const lib = { ...mediaLibrary };
    delete lib['All Tracks'];
    return lib;
  });
  
  const [currentFolder, setCurrentFolder] = useState('My Music');
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  const [showPlaylist, setShowPlaylist] = useState(true);
  const [isLooping, setIsLooping] = useState(false);
  
  const { volume: globalVolume, isMuted, toggleMute } = useVolume();
  
  // Media playback, visualizer, playlist logic
}

Configuration

App Settings

From apps/index.jsx:
MediaPlayer: {
  name: 'MediaPlayer',
  header: { icon: mediaPlayerIcon, title: 'Media Player', invisible: false },
  component: MediaPlayer,
  defaultSize: { width: 800, height: 575 },
  defaultOffset: getCenter(300, 450),
  resizable: true,
  minimized: false,
  maximized: shouldMaximize(300, 450, true),
  multiInstance: false,
  minWidth: 370,
  minHeight: 370,
}

Media Library

From config.js:
const BASE_PATH = import.meta.env.BASE_URL.replace(/\/$/, '');

const createMedia = (type, relativePath, title, artist = '', duration = 0) => ({
  url: `${BASE_PATH}${relativePath}`,
  type,
  title,
  artist,
  duration,
  id: Math.random().toString(36).substr(2, 9),
});

const musicTracks = [
  createMedia('audio', '/music/addiction.wav', 'Addiction', 'Jogeir Liljedahl', 288),
  createMedia('audio', '/music/youwillknow.mp3', 'You Will Know Our Names', 'Kenji Hiramatsu', 161),
  createMedia('audio', '/music/music1.wav', 'Music 1', 'Skillz Productions', 118),
  createMedia('audio', '/music/man.ogg', 'man', 'Toby Fox', 11),
  createMedia('audio', '/music/robocop.mp3', 'robocop.mp3', 'Kombat Unit', 118),
];

const videoTracks = [
  // createMedia('video', '/videos/demo.mp4', 'My Cool Video'),
];

const imageTracks = [
  // createMedia('image', '/photos/wallpaper.jpg', 'Cool Wallpaper'),
];

export const mediaLibrary = {
  'My Music': musicTracks,
  Videos: videoTracks,
  Photos: imageTracks,
  'All Tracks': [...musicTracks, ...videoTracks, ...imageTracks],
};

Media Type Detection

const getMediaType = (url, type) => {
  if (type) {
    if (type.startsWith('video')) return 'video';
    if (type.startsWith('image')) return 'image';
    return 'audio';
  }
  if (!url || typeof url !== 'string') return 'audio';
  if (url.match(/\.(mp4|webm|ogv|mkv)$/i)) return 'video';
  if (url.match(/\.(jpg|jpeg|png|gif|bmp)$/i)) return 'image';
  return 'audio';
};

Audio Visualizer

The visualizer uses Web Audio API to create a frequency spectrum display:
const setupVisualizer = useCallback(() => {
  if (!mediaRef.current || currentItem.type !== 'audio') return;
  
  try {
    if (!audioContextRef.current) {
      const AudioContext = window.AudioContext || window.webkitAudioContext;
      audioContextRef.current = new AudioContext();
    }
    
    const ctx = audioContextRef.current;
    if (ctx.state === 'suspended') ctx.resume();
    
    if (!analyserRef.current) {
      analyserRef.current = ctx.createAnalyser();
      analyserRef.current.fftSize = 2048;
    }
    
    if (connectedElementRef.current !== mediaRef.current) {
      if (sourceRef.current) {
        sourceRef.current.disconnect();
        sourceRef.current = null;
      }
      
      sourceRef.current = ctx.createMediaElementSource(mediaRef.current);
      sourceRef.current.connect(analyserRef.current);
      analyserRef.current.connect(ctx.destination);
      connectedElementRef.current = mediaRef.current;
    }
    
    drawSpectrum();
  } catch (e) {}
}, [currentItem.type, drawSpectrum]);

Spectrum Drawing

const drawSpectrum = useCallback(() => {
  if (!canvasRef.current || !analyserRef.current) return;
  
  const canvas = canvasRef.current;
  const ctx = canvas.getContext('2d');
  const analyser = analyserRef.current;
  const bufferLength = analyser.frequencyBinCount;
  const dataArray = new Uint8Array(bufferLength);
  
  const draw = () => {
    animationRef.current = requestAnimationFrame(draw);
    analyser.getByteFrequencyData(dataArray);
    
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    const barWidth = canvas.width / binsToDraw;
    let x = 0;
    
    for (let i = 0; i < binsToDraw; i++) {
      const barHeight = (dataArray[i] / 255) * canvas.height;
      const gradient = ctx.createLinearGradient(0, canvas.height, 0, 0);
      gradient.addColorStop(0, '#004e92');
      gradient.addColorStop(1, '#50cc7f');
      ctx.fillStyle = gradient;
      
      ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
      x += barWidth;
    }
  };
  draw();
}, []);

Drag & Drop Support

const handleDrop = e => {
  e.preventDefault();
  e.stopPropagation();
  
  const files = Array.from(e.dataTransfer.files).filter(
    f => f.type.startsWith('audio/') ||
         f.type.startsWith('video/') ||
         f.type.startsWith('image/')
  );
  
  if (files.length > 0) {
    const newItems = files.map(file => ({
      url: URL.createObjectURL(file),
      type: getMediaType(null, file.type),
      title: file.name,
      artist: '',
      id: Math.random().toString(36).substr(2, 9),
    }));
    
    if (currentFolder === 'All Media') {
      setUncategorized(prev => [...prev, ...newItems]);
    } else {
      setLibrary(prev => ({
        ...prev,
        [currentFolder]: [...(prev[currentFolder] || []), ...newItems],
      }));
    }
  }
};

<PlayerContainer
  onDragOver={e => e.preventDefault()}
  onDrop={handleDrop}
>
  {/* Player content */}
</PlayerContainer>

Playlist Reordering

Drag and drop to reorder tracks within folders:
const dragItem = useRef(null);
const dragOverItem = useRef(null);

const handleSortStart = (e, position) => {
  dragItem.current = position;
  e.dataTransfer.effectAllowed = 'move';
};

const handleSortEnter = (e, position) => {
  dragOverItem.current = position;
};

const handleSortEnd = () => {
  if (dragItem.current === null || dragOverItem.current === null) return;
  if (currentFolder === 'All Media') return; // Can't reorder in All Media
  
  const newPlaylist = [...library[currentFolder]];
  const draggedItemContent = newPlaylist.splice(dragItem.current, 1)[0];
  newPlaylist.splice(dragOverItem.current, 0, draggedItemContent);
  
  // Update current index if needed
  if (dragItem.current === currentIndex) {
    setCurrentIndex(dragOverItem.current);
  } else if (dragItem.current < currentIndex && dragOverItem.current >= currentIndex) {
    setCurrentIndex(currentIndex - 1);
  } else if (dragItem.current > currentIndex && dragOverItem.current <= currentIndex) {
    setCurrentIndex(currentIndex + 1);
  }
  
  dragItem.current = null;
  dragOverItem.current = null;
  
  setLibrary(prev => ({ ...prev, [currentFolder]: newPlaylist }));
};

<PlaylistItem
  draggable={currentFolder !== 'All Media'}
  onDragStart={e => handleSortStart(e, i)}
  onDragEnter={e => handleSortEnter(e, i)}
  onDragEnd={handleSortEnd}
  onDragOver={e => e.preventDefault()}
>
  {/* Item content */}
</PlaylistItem>

Scrolling Title Ticker

For long track names, the title scrolls automatically:
const TrackTicker = ({ text }) => {
  const [isOverflowing, setIsOverflowing] = useState(false);
  const containerRef = useRef(null);
  const measurerRef = useRef(null);
  
  useLayoutEffect(() => {
    if (containerRef.current && measurerRef.current) {
      const containerWidth = containerRef.current.offsetWidth;
      const contentWidth = measurerRef.current.offsetWidth;
      const shouldScroll = contentWidth > containerWidth;
      setIsOverflowing(prev => (prev !== shouldScroll ? shouldScroll : prev));
    }
  });
  
  const duration = Math.max(5, text.length * 0.25);
  
  return (
    <TickerContainer ref={containerRef}>
      <span ref={measurerRef} style={{ position: 'absolute', visibility: 'hidden' }}>
        {text}
      </span>
      
      {isOverflowing ? (
        <TickerWrapper duration={duration}>
          <TickerItem>{text}</TickerItem>
          <TickerItem>{text}</TickerItem>
        </TickerWrapper>
      ) : (
        <span>{text}</span>
      )}
    </TickerContainer>
  );
};

const scroll = keyframes`
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
`;

const TickerWrapper = styled.div`
  display: inline-flex;
  animation: ${scroll} ${props => props.duration}s linear infinite;
`;

Volume Control

Integrates with global volume system:
const { volume: globalVolume, isMuted, toggleMute } = useVolume();
const [localVolume, setLocalVolume] = useState(1);

useEffect(() => {
  if (mediaRef.current && !isImage) {
    mediaRef.current.volume = (globalVolume / 100) * localVolume;
    mediaRef.current.muted = isMuted;
  }
}, [globalVolume, localVolume, isMuted, isImage, currentItem.id]);

<VolumeContainer>
  <MuteBtn onClick={toggleMute}>
    <SpeakerIcon muted={isMuted || localVolume === 0} />
  </MuteBtn>
  <VolumeSlider
    type="range"
    min={0}
    max={1}
    step={0.05}
    value={localVolume}
    onChange={e => setLocalVolume(parseFloat(e.target.value))}
  />
</VolumeContainer>

Playback Controls

const togglePlay = async () => {
  if (isImage || !mediaRef.current) return;
  
  if (audioContextRef.current?.state === 'suspended') {
    await audioContextRef.current.resume();
  }
  
  if (mediaRef.current.paused) {
    mediaRef.current.play();
    setIsPlaying(true);
    setupVisualizer();
  } else {
    mediaRef.current.pause();
    setIsPlaying(false);
    if (animationRef.current) cancelAnimationFrame(animationRef.current);
  }
};

const stop = () => {
  if (isImage || !mediaRef.current) return;
  mediaRef.current.pause();
  mediaRef.current.currentTime = 0;
  setIsPlaying(false);
  if (animationRef.current) cancelAnimationFrame(animationRef.current);
};

const nextTrack = () =>
  playlist.length && setCurrentIndex(p => (p + 1) % playlist.length);

const prevTrack = () =>
  playlist.length && setCurrentIndex(p => (p - 1 + playlist.length) % playlist.length);

const handleEnded = () => {
  if (isLooping && mediaRef.current) {
    mediaRef.current.currentTime = 0;
    mediaRef.current.play();
  } else {
    nextTrack();
  }
};

Media Rendering

const renderMedia = () => {
  if (!currentItem.url) return <VisualizerContainer />;
  
  const commonProps = {
    key: currentItem.id || currentItem.url,
    ref: mediaRef,
    onTimeUpdate: () =>
      mediaRef.current &&
      (setCurrentTime(mediaRef.current.currentTime),
      setDuration(mediaRef.current.duration || 0)),
    onEnded: handleEnded,
    onPlay: () => {
      setIsPlaying(true);
      setupVisualizer();
    },
    onPause: () => setIsPlaying(false),
    src: currentItem.url,
    crossOrigin: 'anonymous',
  };
  
  if (currentItem.type === 'video')
    return <VideoElement {...commonProps} onClick={togglePlay} />;
  
  if (currentItem.type === 'image')
    return <ImageElement src={currentItem.url} />;
  
  return (
    <VisualizerContainer>
      <audio {...commonProps} />
      <canvas
        ref={canvasRef}
        width={windowSize.width}
        height={Math.max(100, windowSize.height - (showPlaylist ? 180 : 80))}
      />
    </VisualizerContainer>
  );
};

Folder Management

const changeFolder = folderName => {
  if (folderName === currentFolder) return;
  setIsPlaying(false);
  setCurrentIndex(0);
  setCurrentTime(0);
  setCurrentFolder(folderName);
};

<FolderBar>
  {Object.keys(library).map(folder => (
    <FolderTab
      key={folder}
      active={folder === currentFolder}
      onClick={() => changeFolder(folder)}
    >
      {folder}
    </FolderTab>
  ))}
  <FolderTab
    active={currentFolder === 'All Media'}
    onClick={() => changeFolder('All Media')}
  >
    All Media
  </FolderTab>
</FolderBar>

Usage Example

import { MediaPlayer } from 'src/WinXP/apps';

function Desktop() {
  return (
    <Window title="Media Player">
      <MediaPlayer />
    </Window>
  );
}

Responsive Features

The player adapts to window size:
const [windowSize, setWindowSize] = useState({ width: 300, height: 300 });

useEffect(() => {
  if (!containerRef.current) return;
  resizeObserverRef.current = new ResizeObserver(entries => {
    for (let entry of entries) {
      setWindowSize({
        width: Math.floor(entry.contentRect.width),
        height: Math.floor(entry.contentRect.height),
      });
    }
  });
  resizeObserverRef.current.observe(containerRef.current);
  return () => resizeObserverRef.current?.disconnect();
}, []);

const compactMode = windowSize.width < 350;

<ButtonRow compact={compactMode}>
  {/* Controls adjust spacing based on window size */}
</ButtonRow>

Build docs developers (and LLMs) love