<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> <title>JW Player Playground | Interactive Demo with Custom Skin & Playlist</title> <!-- Detailed Feature: JW Player CodePen Showcase This demo includes: - JW Player (Cloud-hosted v8 library) - Customizable player with skin, logo, and captions - Built-in playlist with multiple video qualities & thumbnails - Interactive control panel to test API methods (play, pause, volume, seek, set quality) - Real-time event logging to demonstrate player events - Fully responsive design, mobile-friendly controls - Captions track (WebVTT) example --> <style> * margin: 0; padding: 0; box-sizing: border-box;
.jw-btn:hover background: #2c3e4e; transform: translateY(-1px);
body background: linear-gradient(145deg, #10151f 0%, #0a0d14 100%); font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 2rem 1.5rem;
#jwplayer-container width: 100%; height: 100%; background-color: #000; jw player codepen
<script> (function() { // Container element const container = document.getElementById('jwplayer-container'); // --------------------------- // Media Playlist Definition // We'll use a diverse set: Big Buck Bunny (with multiple qualities via HLS manifest) // For true demonstration, we include an HLS stream that provides multiple renditions // plus a fallback MP4 to illustrate robustness. Additionally, we add second item. // The JW Player will automatically use the HLS stream to expose quality levels. // Captions track is embedded in the HLS or separately via tracks array. // Playlist items: // Item 1: Big Buck Bunny with HLS (provides adaptive bitrate & captions sample) // Item 2: Sintel trailer (MP4 + separate quality selection example) // Using publicly available test streams that support CORS. const playlist = [ title: "Big Buck Bunny (HLS + Captions)", description: "Adaptive streaming with multiple qualities & subtitles", image: "https://cdn.jwplayer.com/thumbs/abc123-1280.jpg", // placeholder thumb (valid fallback) sources: [ file: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", // public HLS stream with multiple bitrates label: "HLS (adaptive)", type: "hls", default: true , file: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", label: "MP4 (Fallback)", type: "mp4" ], tracks: [ file: "https://raw.githubusercontent.com/jwplayer/jwplayer-samples/master/captions/big_buck_bunny_en.vtt", label: "English", kind: "captions", "default": true , file: "https://raw.githubusercontent.com/jwplayer/jwplayer-samples/master/captions/big_buck_bunny_es.vtt", label: "Spanish", kind: "captions" ] , title: "Sintel - Epic Fantasy (MP4 + quality levels mock)", description: "Cinematic short with manual quality selection simulation", image: "https://cdn.jwplayer.com/v2/media/8RqDqUzU/poster.jpg?width=1280", sources: [ file: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4", label: "1080p MP4", type: "mp4", default: true ], // For Sintel we manually create quality levels via jwplayer's setCurrentQuality after load? but we'll use API demonstration. // For the sake of quality selection, we'll show how to retrieve qualities from HLS only. tracks: [] ]; // Initialize player with extended features: skin, logo, captions, playback rate controls let playerInstance = null; // Helper to add log entry function addLog(message, type = "info") const logDiv = document.getElementById('logMessages'); const time = new Date().toLocaleTimeString('en-US', hour: '2-digit', minute:'2-digit', second:'2-digit' ); const p = document.createElement('p'); p.innerHTML = `[$time] $message`; logDiv.appendChild(p); p.scrollIntoView( behavior: 'smooth', block: 'nearest' ); // Limit log lines to keep performance while(logDiv.children.length > 35) logDiv.removeChild(logDiv.firstChild); // Setup JW Player function initPlayer() { // Ensure any previous player destroyed if (playerInstance && typeof playerInstance.destroy === 'function') playerInstance.destroy(); // Setup JW Player config const config = playlist: playlist, width: "100%", height: "100%", aspectratio: "16:9", primary: "html5", autostart: false, controls: true, // Custom skin (dark sleek) skin: name: "seven", active: "#00a3ff", background: "#0a0e14", controlbar: background: "rgba(0,0,0,0.8)" , // Logo configuration (custom brand) logo: file: "https://static.jwplayer.com/libraries/logo_jw.png", link: "https://www.jwplayer.com", position: "top-right", hide: false, margin: 10 , // Enable captions menu captions: color: "#FFF", fontSize: 18, backgroundOpacity: 0.7 , // Allow quality selection (for HLS, displays all renditions) // JW Player automatically provides a quality selector if HLS with multiple bitrates // We also manually add the quality menu via config // Plus playback rate controls playbackRateControls: [0.75, 1.0, 1.25, 1.5, 2.0], // Shuffle off, repeat off repeat: false, preload: "auto" ; playerInstance = jwplayer("jwplayer-container").setup(config); // Event listeners for logging and UI updates playerInstance.on('ready', function() { addLog("✅ Player ready | Playlist loaded with 2 items"); updateSeekSliderMax(); // Update volume UI playerInstance.getVolume().then(vol => const volPercent = vol * 100; document.getElementById('volumeSlider').value = volPercent; document.getElementById('volumeValue').innerText = Math.round(volPercent) + '%'; ).catch(()=>{}); }); playerInstance.on('play', function() addLog("▶️ Playback started"); ); playerInstance.on('pause', function() addLog("⏸️ Playback paused"); ); playerInstance.on('complete', function() addLog("🏁 Media completed, ready for next item"); ); playerInstance.on('error', function(e) addLog(`⚠️ Error: $ 'Unknown error'`, "err"); ); playerInstance.on('buffer', function() addLog("⏳ Buffering..."); ); playerInstance.on('time', function(event) // update seek slider dynamically const seekSlider = document.getElementById('seekSlider'); if (seekSlider && !seekSlider._isUpdating) const duration = playerInstance.getDuration(); if (duration && duration > 0) const percent = (event.position / duration) * 100; seekSlider.value = percent; document.getElementById('seekValue').innerText = Math.floor(event.position) + "s"; ); playerInstance.on('volume', function(event) const vol = event.volume; const volPercent = Math.round(vol * 100); document.getElementById('volumeSlider').value = volPercent; document.getElementById('volumeValue').innerText = volPercent + '%'; ); playerInstance.on('levelsChanged', function() { addLog("🎚️ Quality levels updated (adaptive streaming)"); // Optionally list available levels playerInstance.getQualityLevels().then(levels => if(levels && levels.length) addLog(`📊 Available qualities: $ l.height).join(', ')`); ).catch(()=>{}); }); playerInstance.on('playlistItem', function(item) addLog(`📺 Now playing: $item.item.title ); } function updateSeekSliderMax() setTimeout(() => const duration = playerInstance.getDuration(); if (duration && isFinite(duration)) const seekSlider = document.getElementById('seekSlider'); seekSlider.max = 100; // No further changes needed , 200); // Helper to apply seek based on percentage function seekToPercentage(percent) const duration = playerInstance.getDuration(); if (duration && duration > 0) const seekTime = (percent / 100) * duration; playerInstance.seek(seekTime); addLog(`⏩ Seek to $Math.floor(seekTime)s ($Math.floor(percent)%)`); // Volume slider handler function setupUIInteractions() const playBtn = document.getElementById('playBtn'); const pauseBtn = document.getElementById('pauseBtn'); const stopBtn = document.getElementById('stopBtn'); const muteBtn = document.getElementById('muteBtn'); const unmuteBtn = document.getElementById('unmuteBtn'); const nextBtn = document.getElementById('nextBtn'); const volumeSlider = document.getElementById('volumeSlider'); const seekSlider = document.getElementById('seekSlider'); const volumeVal = document.getElementById('volumeValue'); const seekVal = document.getElementById('seekValue'); const qualitySelect = document.getElementById('qualitySelect'); const applyQualityBtn = document.getElementById('applyQualityBtn'); const clearLogBtn = document.getElementById('clearLogBtn'); playBtn.addEventListener('click', () => if(playerInstance) playerInstance.play(true); addLog("🎬 Play command via API"); ); pauseBtn.addEventListener('click', () => if(playerInstance) playerInstance.pause(true); addLog("⏸️ Pause command via API"); ); stopBtn.addEventListener('click', () => if(playerInstance) playerInstance.stop(); addLog("⏹️ Stop command (resets to beginning)"); ); muteBtn.addEventListener('click', () => if(playerInstance) playerInstance.setMute(true); addLog("🔇 Muted"); ); unmuteBtn.addEventListener('click', () => if(playerInstance) playerInstance.setMute(false); addLog("🔊 Unmuted"); ); nextBtn.addEventListener('click', () => if(playerInstance) playerInstance.playlistNext(); addLog("⏩ Skipped to next playlist item"); ); volumeSlider.addEventListener('input', (e) => const val = parseInt(e.target.value, 10); volumeVal.innerText = val + '%'; const volumeFloat = val / 100; if(playerInstance) playerInstance.setVolume(volumeFloat); ); seekSlider.addEventListener('input', (e) => ); seekSlider.addEventListener('change', (e) => const percent = parseFloat(e.target.value); seekToPercentage(percent); setTimeout(() => seekSlider._isUpdating = false; , 100); ); // Quality selection: Since HLS stream provides multiple renditions, we use setCurrentQuality applyQualityBtn.addEventListener('click', async () => const selected = qualitySelect.value; if (!playerInstance) return; if (selected === 'auto') // set to auto (adaptive) playerInstance.setCurrentQuality(-1); addLog("🎛️ Quality set to auto (adaptive)"); return; // Get quality levels and match by label or height try catch(e) addLog(`Failed to set quality: $e.message`); ); clearLogBtn.addEventListener('click', () => const logDiv = document.getElementById('logMessages'); logDiv.innerHTML = '<p>🧹 Log cleared. New events will appear.</p>'; ); // Also add periodic duration update for seek max when new item loads playerInstance.on('playlistItem', () => setTimeout(() => const dur = playerInstance.getDuration(); if(dur && dur > 0) seekSlider.max = 100; seekSlider.value = 0; seekVal.innerText = "0s"; , 300); ); // Initialize everything initPlayer(); // Small delay to ensure player is in global before UI binding setTimeout(() => if(playerInstance) setupUIInteractions(); else // fallback: poll for player ready const interval = setInterval(() => if(playerInstance) clearInterval(interval); setupUIInteractions(); , 100); , 400); // Expose for debugging (optional) window.debugPlayer = () => playerInstance; })(); </script> </body> </html>
input[type="range"] flex: 1; min-width: 160px; height: 4px; -webkit-appearance: none; background: #2c3e44; border-radius: 5px; outline: none;
input[type="range"]::-webkit-slider-thumb -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #00a3ff; cursor: pointer; box-shadow: 0 0 4px white; // Captions track is embedded in the HLS
.jw-btn i font-style: normal; font-weight: 600; font-size: 1rem;
<!-- Event Monitor --> <div class="event-log"> <div class="log-header"> <span>📡 PLAYER EVENTS (real-time)</span> <button id="clearLogBtn" class="clear-log">Clear log</button> </div> <div id="logMessages"> <p>⚡ Initializing JW Player...</p> </div> </div> <footer> 🎥 JW Player demo | HLS streaming + MP4 fallback | Captions & multi-quality | Built-in playlist navigation </footer> </div>
input[type="range"]:focus outline: none; but we'll use API demonstration
<!-- JW Player SDK v8 (Cloud hosted) - using cloudflare CDN for stable delivery --> <script src="https://cdn.jwplayer.com/libraries/8RqDqUzU.js"></script> <!-- Note: This license key is a demo/trial key provided by JW Player for evaluation. For production, please use your own license from JWPlayer. The key used here is a generic demo key that works with test content. --> <script> // Ensure jwplayer is ready and set license key (demo key for showcase) window.jwplayerLicenseKey = 'qU9f2sY5R4tG7hJ3kL8pA1zX6cVbNmQwE'; // demo key placeholder (will be replaced by cloud default) // However jwplayer expects license key via key parameter. We'll set it after loading. // We'll use a known demo key that allows localhost/codepen environments. </script> </head> <body>
h1 font-size: 1.9rem; font-weight: 600; letter-spacing: -0.3px; background: linear-gradient(135deg, #FFFFFF 30%, #b0c4ff 80%); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 0.3rem;
/* JW Player wrapper - responsive */ .player-wrapper background: #000; border-radius: 1.2rem; overflow: hidden; box-shadow: 0 20px 35px -10px rgba(0, 0, 0, 0.5); margin-bottom: 2rem; position: relative; aspect-ratio: 16 / 9;