mztx/shinage-player-bundle
Symfony bundle providing a digital signage player for the Shinage system by mztx. It plays scheduled media playlists on displays, integrates with Symfony applications, and handles playback and scheduling for screen-based signage setups.
It is not a Laravel package. mztx/shinage-player-bundle is a Symfony bundle (Bundle suffix, DI extension, Twig templates, likely Doctrine entities). There is no service provider, no config/*.php to publish, no auto-discovery. So "getting started" means reading and extracting, not composer require.
Minimal first steps:
Clone, don't require.
git clone https://github.com/michz/shinage-player-bundle /tmp/shinage
Keep it out of composer.json on day one. Archive the repo internally if you intend to use anything from it.
Read in this order to map what is actually portable:
composer.json — PHP/Symfony constraints, license, orphaned deps (watch for symfony/symfony meta-package).*/Resources/config/services.yml (or .xml) — tells you what services exist and therefore what the domain surface is.DependencyInjection/*Extension.php — the real config schema; this is your requirements list.Resources/views/** — Twig templates. This is usually the genuinely reusable part.Resources/public/** or assets/** — the player JS/CSS. Extract this if anything is worth keeping.Entity/** — Doctrine models (Screen, Playlist, Media). Readable as a domain model; not portable without a rewrite.First use case — the portable slice only: render the player front-end as a Blade component against fixture data.
{{-- resources/views/components/signage-player.blade.php --}}
<div class="shinage-player" data-playlist='@json($playlist)'>
@foreach ($playlist->items as $item)
<div class="player-item" data-src="{{ $item->url }}" data-duration="{{ $item->duration }}"></div>
@endforeach
</div>
Success criterion for the first session: the player surface renders and plays from static data. Not architectural parity, not the Symfony wiring.
Where to look first for the data contract: whatever the Twig templates and JS actually consume. That shape (playlist → items → media url/duration/type, plus screen config) is the thing you are porting. Write it down before writing any code.
Pattern 1 — Reference spike, not dependency (default). Timebox 1–2 days. Output is an ADR plus a data-contract note, not a PR into the app. This is the right move for an archived, 1-star, 0-dependent package: the value is the domain model, and you keep the operational risk at zero.
Pattern 2 — Blade wrapper around the extracted player surface.
Port templates to Blade and assets to resources/js / resources/css, then expose one component with a documented prop contract:
// <x-signage-player :playlist="$playlist" :screen="$screen" />
class SignagePlayer extends Component
{
public function __construct(public Playlist $playlist, public Screen $screen) {}
}
Mount routes behind a feature flag so reversing the decision is one file, not a refactor:
Route::middleware(['web', 'can:signage.view'])
->group(function () {
Route::get('/screens/{screen}/play', ScreenPlayerController::class)
->middleware('feature:signage');
});
Pattern 3 — Port the domain behind interfaces you own. If the scheduling/playlist logic turns out to be non-trivial, wrap it rather than copy it inline. Define the boundary first, implement with Eloquent:
interface PlaylistResolver { public function forScreen(Screen $screen, CarbonImmutable $at): Playlist; }
interface MediaUrlResolver { public function url(Media $media): string; }
This keeps ported logic testable in isolation and makes the ported-vs-native decision reversible.
Pattern 4 — Decouple the player from the app release cadence. For signage, the player is best deployed as its own artifact (static bundle or iframe) that Laravel feeds with JSON. Signage screens are remote and hard to visit; you do not want a Laravel deploy to be a prerequisite for a player fix. A small JSON endpoint is the clean seam:
Route::get('/api/screens/{screen}/manifest', ScreenManifestController::class);
Pattern 5 — Health and heartbeat in the shell, not the bundle. The recurring operational burden of signage is remote failure. Build in, from the first version: last-seen heartbeat from the player, a manifest version/etag, and an error hook. None of this comes from the bundle; plan it as part of your wrapper.
Pattern 6 — Map concepts before copying code.
Read Entity/ and the DI extension to enumerate: what is a Screen, a Playlist, a Media, a Schedule, a Device? Then decide which of these you actually need in v1. Do not port a device-management model if the requirement is one screen in a lobby.
Anti-pattern to avoid: trying to run the bundle under a Symfony/Laravel bridge. For an archived 1-star package, that is a large amount of glue for a dependency you already decided not to maintain.
Symfony ≠ Laravel, and this is the headline gotcha. No service provider, no facades, no config publishing. Anything Doctrine-bound or container-bound is a rewrite, not a port. Budget accordingly: templates and JS are days; the domain model is weeks.
Archived and unmaintained. No security response, no PHP 8.x/Symfony 5–7 guarantees, no issue triage. If you vendor any of it, you own it on day one. Assign an explicit code owner; an unowned fork of an archived package is the worst of both worlds.
Check the license before copying anything. Vendoring templates/assets is a distribution event. If the license is unclear or incompatible, the spike ends there.
composer.json archaeology. Expect PHP 7.x-era constraints and possibly the abandoned symfony/symfony meta-package. If you ever do try to install it, transitive CVEs come with it and cannot be patched upstream. Prefer extracting the front-end over installing the bundle.
Twig → Blade translation is mechanical but not free. {{ }} is shared; {% %} becomes @directive. Macros and include chains need attention and are where the time goes. Do a full template inventory before estimating.
Autoplay and kiosk browser drift are the silent killers. Unattended Chromium on signage hardware has stricter autoplay behaviour and older engines. Test on the target device, not just your laptop. A player that works in Chrome desktop can go black on a screen in a hallway.
Codec support on signage hardware lags. A media format that plays in dev may not play on an embedded device. Keep the media pipeline boring (H.264/MP4 where possible) and validate against the actual device early.
Design for offline/degraded network. A live request per playback from Laravel is a design gap: the player should cache the manifest and media locally. If the bundle assumes online-only, fix that in your wrapper rather than inheriting it.
Asset versioning. Cache the player bundle and playlists separately with explicit versions/etags. A common failure is a stale cached player meeting a new playlist schema — one field rename and the screen goes dark.
CI coverage for the player surface. A headless-browser "does the player render and advance to the next item" check is worth more than unit tests here. It catches the class of failures that only show up remotely.
Intent archaeology is the real ramp-up cost. Small codebase, ordinary concepts (screen, playlist, media), but with no docs, issues, or maintainer, understanding why specific behaviours exist means reading the whole thing. Capture findings in an ADR during the spike so the knowledge survives the engineer.
Look for a successor before committing. The broader Shinage project may have a maintained player, or there may be a standalone JS artifact (npm/build) you can depend on directly, bypassing the bundle entirely. Also check maintained OSS alternatives (Xibo, Anthias) and, for the player itself, video.js/hls.js — often the honest answer is a maintained JS player plus your own Laravel admin layer.
Decision gate, explicit. If the spike shows the player logic is thin or you already have an equivalent media component, stop. The package's value here is as a requirements inventory, not as code.
How can I help you explore Laravel packages today?