happyworm/jplayer
jPlayer is a free, open-source jQuery plugin for playing audio and video on the web, offering a single cross-browser API over HTML5 media with a Flash fallback for legacy browsers. Fully skinnable, accessible, and easy to drop into any site.
jPlayer is a jQuery HTML5 audio/video plugin, not a Laravel package. There is no service provider, facade, config file, or artisan command. You integrate it as a front-end asset and drive it from Blade/JavaScript.
Minimal setup:
Install the front-end dependencies in your Laravel app:
npm install jquery jplayer
Or load jQuery and jPlayer 2.9.2 from a CDN in a Blade layout if you do not want to bundle them.
Expose jQuery and import jPlayer in resources/js/app.js:
import $ from 'jquery';
window.$ = window.jQuery = $;
import 'jplayer';
If the npm import path is awkward, copy node_modules/jplayer/dist/jplayer/ and the desired skin into public/vendor/jplayer/ and reference them with asset().
Create a Blade partial with the required jPlayer markup:
<div id="jp_container_{{ $id }}" class="jp-audio" role="application" aria-label="audio player">
<div class="jp-type-single">
<div id="jquery_jplayer_{{ $id }}" class="jp-jplayer"></div>
<div class="jp-gui jp-interface">
<div class="jp-controls">
<button class="jp-play" role="button" tabindex="0">play</button>
<button class="jp-pause" role="button" tabindex="0">pause</button>
<div class="jp-progress"><div class="jp-seek-bar"><div class="jp-play-bar"></div></div></div>
<div class="jp-time-holder"><div class="jp-current-time"></div><div class="jp-duration"></div></div>
</div>
</div>
</div>
</div>
Initialize after DOM ready, preferably in a dedicated module such as resources/js/player.js:
export function initAudioPlayer(root) {
const $root = window.jQuery(root);
const id = $root.data('player-id');
const $player = window.jQuery(`#jquery_jplayer_${id}`);
$player.jPlayer({
ready: function () {
this.jPlayer('setMedia', {
mp3: $root.data('src'),
});
},
supplied: 'mp3',
solution: 'html',
preload: 'metadata',
volume: 0.8,
useStateClassSkin: true,
autoBlob: true,
});
$root.on('click', '.jp-play', function () {
$player.jPlayer('play');
});
$root.on('click', '.jp-pause', function () {
$player.jPlayer('pause');
});
}
First use case: a single-track audio player on a show/episode page. Pass the media URL from the controller, render the partial, initialize on page load. Once that works, move to playlists and custom controls.
Where to look first:
setMedia, play, pause, stop, destroy, and the event list (ready, play, pause, ended, timeupdate, error).dist/skin/ folder for markup and CSS examples. The skin CSS assumes specific class names, so start from a skin and customize rather than inventing markup.vite.config.js, resources/js/app.js, resources/css/app.css, and how you publish static assets to public/.Blade partial or component with props. Wrap jPlayer markup in resources/views/components/audio-player.blade.php and accept src, id, poster, title, and type. Generate unique IDs with Str::uuid() or an explicit counter so multiple players can coexist.
<x-audio-player
:id="$episode->id"
:src="route('episodes.audio', $episode)"
:title="$episode->title"
/>
Data-driven playlists. Serialize playlist data from the controller and pass it through a data attribute or a JSON script block. Keep initialization logic in JavaScript and avoid inlining complex JavaScript in Blade.
<div
id="player-{{ $playlist->id }}"
data-player
data-playlist='@json($playlist->tracks->map(fn ($t) => ["title" => $t->title, "mp3" => $t->url]))'
></div>
const tracks = JSON.parse(root.dataset.playlist);
let index = 0;
function setTrack(i) {
index = i;
$player.jPlayer('setMedia', tracks[i]);
}
$player.on($.jPlayer.event.ended, () => {
if (index + 1 < tracks.length) {
setTrack(index + 1);
$player.jPlayer('play');
}
});
Reusable factory. Create one function createJPlayer(element, options) that returns the jQuery player instance and wires custom controls. This keeps single-track, playlist, and video pages consistent.
Custom controls instead of skin defaults. Bind to jPlayer events and update your own UI. This is usually cleaner in Laravel apps than relying on the old default skins.
$player.on($.jPlayer.event.timeupdate, (event) => {
const percent = event.jPlayer.status.currentPercentAbsolute;
$('.jp-play-bar').css('width', `${percent}%`);
$('.jp-current-time').text($.jPlayer.convertTime(event.jPlayer.status.currentTime));
});
Seeking. Implement a click on the progress bar and call $player.jPlayer('play', percent) or $player.jPlayer('play', seconds).
Dynamic media without page reloads. For episode lists, bind click handlers that call setMedia() on the existing player. This avoids destroying and recreating the player on every selection.
document.querySelectorAll('[data-track]').forEach((button) => {
button.addEventListener('click', () => {
$player.jPlayer('setMedia', { mp3: button.dataset.track });
$player.jPlayer('play');
});
});
Private or signed audio URLs. jPlayer is just an HTML5 media element underneath. Serving private files through a Laravel route works, but the response must support byte ranges for seeking. response()->file($path) and BinaryFileResponse generally handle this; custom streamed responses need explicit Range handling and 206 Partial Content.
Route::get('/episodes/{episode}/audio', function (Episode $episode) {
abort_unless(auth()->user()->can('listen', $episode), 403);
return response()->file(
Storage::disk('private')->path($episode->audio_path),
['Content-Type' => 'audio/mpeg']
);
})->middleware(['auth', 'signed'])->name('episodes.audio');
Livewire, Alpine, Inertia, and Turbo. Initialize jPlayer when the component mounts and destroy it when it unmounts. In Livewire, wrap the player markup in wire:ignore so Livewire does not replace the jPlayer-generated DOM. Re-initialize after livewire:navigated.
document.addEventListener('livewire:navigated', () => {
document.querySelectorAll('[data-player]').forEach(initAudioPlayer);
});
For Alpine:
<div
x-data
x-init="window.initAudioPlayer($el)"
x-on:destroy="window.destroyAudioPlayer($el)"
></div>
Multiple players on one page. Use unique IDs for both the jPlayer element and the container. Scope all jQuery selectors to the container, not the document. If you use the default skins, duplicate IDs will break controls silently.
Video usage. The same API applies. Pass m4v, webmv, or ogv sources plus a poster. Size the player with CSS and ensure the container has explicit dimensions.
Analytics and lifecycle hooks. Use jPlayer events to fire analytics: play, pause, ended, timeupdate at thresholds, and error. This is often easier than instrumenting native media elements manually.
The project is effectively unmaintained. The last release was December 2014. It depends on jQuery, has no TypeScript definitions, no modern module ergonomics, and no Laravel integration. For new projects, compare against native <audio>, Plyr, Video.js, or Howler.js before committing. Use jPlayer when you need its legacy skin ecosystem or existing behavior.
jQuery must be global before jPlayer loads. With Vite, import order matters. If jPlayer reports jQuery is not defined, expose jQuery on window before importing jPlayer. Avoid loading two jQuery copies.
Remove Flash assumptions. Flash is dead. Set solution: 'html' and do not configure swfPath. Old tutorials and skins may still reference Flash fallbacks.
Autoplay is blocked. Browsers require a user gesture for audible playback. Do not depend on autoplay: true or the HTML autoplay attribute. Start playback from a click handler. Muted autoplay is possible natively but jPlayer’s API makes it awkward.
Codec and MIME support vary. Supply multiple sources when possible (mp3, m4a, oga). Ensure your web server sends correct Content-Type headers. Safari does not support Ogg Vorbis; HLS is not supported by jPlayer.
Seeking needs HTTP range support. If seeking fails or the duration is wrong, check the network tab for 206 Partial Content and Accept-Ranges. Laravel’s response()->file() is usually sufficient; hand-rolled stream responses often are not.
Skin CSS background images can break under Vite. jPlayer skins reference sprite images relative to the CSS file. If Vite hashes or relocates assets, copy the full skin directory into public/vendor/jplayer/ and reference the public CSS instead of importing it through the bundler.
setMedia before ready can be lost. Initialize inside the ready callback or wait for the ready event before calling setMedia. Calling play() immediately after setMedia() can also fail until the media is supplied.
Debug with jPlayer events. Bind to $.jPlayer.event.error and log event.jPlayer.error. Check event.jPlayer.status for current state. The supplied option must match the media object keys exactly (mp3, m4a, oga, wav, webma, m4v, webmv, ogv).
Duplicate IDs cause silent control failures. Every player instance needs a unique jquery_jplayer_N and jp_container_N. Scope event handlers to the container. When rendering lists, use the model ID or a UUID.
Livewire and SPA navigation leak players. If you do not call destroy() and unbind events, players accumulate. Wrap the player in wire:ignore, destroy on unmount, and re-initialize on navigation.
Do not inline untrusted URLs. Escape Blade output and avoid interpolating user-controlled media URLs directly into inline JavaScript. Prefer data attributes or @json with proper escaping. Signed URLs are a good fit for private media.
Accessibility needs extra work. The default skins are old and not fully accessible. Add visible focus styles, keyboard handlers for play/pause/seek, ARIA labels, and a transcript link where appropriate.
License is marked NOASSERTION in the package metadata. jPlayer has historically been described as GPL/MIT dual-licensed, but verify the actual license files before redistribution. Do not assume MIT solely from the repository description.
Testing is mostly integration-level. Unit testing jPlayer behavior is awkward. Use Laravel Dusk or browser tests for play/pause/seek flows, and feature tests for the media-serving route, authorization, range requests, and signed URL behavior.
How can I help you explore Laravel packages today?