davidhirtz / yii2-media-video
Video extension for media management module based on Yii 2.0 framework
Package info
github.com/davidhirtz/yii2-media-video
Type:yii2-extension
pkg:composer/davidhirtz/yii2-media-video
Requires
- php: ^8.3
- davidhirtz/yii2-media: ^3.20
- davidhirtz/yii2-skeleton: ^3.20
- james-heinrich/getid3: ^1.9
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
- symfony/browser-kit: ^7.4
- symfony/css-selector: ^7.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Adds HTML video to yii2-media: a file with a video extension is
accepted by the upload, rendered as a <video> tag by the media widget on the frontend, and previewed with a
player in the admin. Its admin thumbnail is a frame the browser captures from the local file while it uploads
(VideoPreviewAssetBundle, no server software needed), and the video's own first frame when the browser could not
decode it. The dimensions of an uploaded video are read with
getID3. The bundle depends on davidhirtz/yii2-media and the
skeleton it brings along; it has no module and no migrations of its own.
Installation
composer require davidhirtz/yii2-media-video
The bundle bootstraps itself through extra.bootstrap (Hirtz\Media\Video\Bootstrap). Nothing needs to run
afterwards: there are no migrations; file/duration reads the duration of videos uploaded before.
Configuration
There is no module. Bootstrap carries the one setting. Yii builds the extension bootstrap through
Yii::createObject(), so a container definition for Hirtz\Media\Video\Bootstrap configures it:
| Property | Default | Meaning |
|---|---|---|
Bootstrap::$allowedVideoExtensions |
['mp4', 'webm', 'ogg'] |
Extensions appended to modules.media.allowedExtensions when the project sets none |
'container' => ['definitions' => [ \Hirtz\Media\Video\Bootstrap::class => ['allowedVideoExtensions' => ['mp4', 'webm']], ]],
Allowed extensions
Bootstrap appends $allowedVideoExtensions to Hirtz\Media\Module::$allowedExtensions when the media
module is initialized, but only while the project does not configure modules.media.allowedExtensions
itself. A project setting its own list names the video extensions there:
'modules' => [ 'media' => [ 'allowedExtensions' => ['jpg', 'jpeg', 'png', 'svg', 'webp', 'mp4', 'webm'], ], ],
Container overrides
Bootstrap::getDefaultConfig() re-points four media classes to their video-aware subclasses through the DI
container. These are defaults merged under the application's configuration, so a definition the project declares
for one of them wins:
| Media class | Video subclass |
|---|---|
Hirtz\Media\Widgets\Media |
Hirtz\Media\Video\Widgets\Media |
Hirtz\Media\Modules\Admin\Widgets\Forms\Fields\FilePreviewField |
Hirtz\Media\Video\Modules\Admin\Widgets\Forms\Fields\FilePreviewField |
Hirtz\Media\Modules\Admin\Widgets\Grids\Columns\Thumbnail |
Hirtz\Media\Video\Modules\Admin\Widgets\Grids\Columns\Thumbnail |
Hirtz\Media\Console\Controllers\FileController |
Hirtz\Media\Video\Console\Controllers\FileController |
Hirtz\Media\Modules\Admin\Widgets\Navs\FileActionDropdown |
Hirtz\Media\Video\Modules\Admin\Widgets\Navs\FileActionDropdown |
A project with a subclass of its own extends the video class instead, or declares its own definition in
container.definitions and takes over the video handling itself.
Subtitles
Every video file has a translated subtitles attribute (Models\CustomAttributes\SubtitlesCustomAttribute,
appended through modules.media.fileCustomAttributes): one WebVTT file per content language, uploaded on the file
page by editors holding File::AUTH_FILE. A file must end in .vtt and begin with WEBVTT. The subtitles stay with
the file when its upload is replaced.
Over MCP, get_record answers each language's subtitles as their URL, and two tools beside the file resource
(File::AUTH_FILE only) read and write the text an agent translates: get_subtitles (file, language) answers the
WebVTT text, set_subtitles (file, language, vtt, null removes) stores it through the same validation as an
admin upload.
Duration
Every video file has a read-only duration attribute (Models\CustomAttributes\DurationCustomAttribute) in whole
seconds, read from the upload by getID3 and shown as H:MM:SS in the admin; agents read it through get_record.
Videos stored before the bundle read it get it from ./yii file/duration.
Streaming
Adaptive video (HLS) comes from an external provider through the videoStream component
(Streaming\StreamManager). Without it nothing changes; with it every video is submitted to the provider after its
upload and played from the provider's playlist once encoded, the local file staying the master copy and the fallback.
A provider is a class implementing Streaming\StreamProviderInterface, usually its own bundle that configures the
component from its params:
'components' => [ 'videoStream' => [ 'class' => \Hirtz\Media\Video\Streaming\StreamManager::class, 'provider' => ['class' => \Vendor\StreamProvider::class], ], ],
| Property | Default | Meaning |
|---|---|---|
provider |
The provider: an instance, a class name, a configuration array or a closure answering one | |
submitByUrl |
true |
The provider fetches the file by its URL; false leaves an upload pending for the console |
webhookEnabled |
true |
Passes the webhook URL to the provider; false where none reaches the installation |
- The state is the read-only
streamattribute of every video file (Models\CustomAttributes\StreamCustomAttribute): provider, remote id, status (pending,processing,ready,failed) and error. The admin shows the status, agents read it throughget_record. - A replaced upload deletes the old remote video and submits the new one; deleting the file deletes its remote video; a duplicate gets a remote video of its own. A failing provider fails the stream, never the upload.
- The provider calls
POST /video-stream/webhook(no CSRF, no session, always204); the payload only names the video, whose state is then fetched from the provider. - Console (
./yii video-streamlists the commands):video-stream/submituploads the videos left pending (--failedadds the failed ones,--allevery video without a stream,--file-idresubmits one),video-stream/refreshfetches the state of every video still pending or processing (for an installation no webhook reaches),video-stream/webhookregisters the webhook URL with the provider, andvideo-stream/setup(the account key from--account-key,VIDEO_STREAM_ACCOUNT_KEYor a hidden prompt) creates the installation's own scope at a provider implementingProvisionableStreamProviderInterfaceand saves its params toconfig/params.php(--configfor another file); an installation already set up is replaced only after an interactive confirmation. All needurlManager.hostInfo. video-stream/orphanslists the provider's videos no file holds, older than an hour, for a provider implementingListableStreamProviderInterface;--deletedeletes them (after a confirmation when interactive).- A stream that does not load falls back to the local file:
Widgets\Mediawrites it todata-fallback, and the script switches to it on a fatal hls.js error or the native player'serror. - A provider refusing for now (a rate limit) answers
pendingwithout an id:video-stream/submitstops there with exit code 75 (TEMPFAIL) and the next run continues with the videos left. - A provider implementing
ProvisionableStreamProviderInterfacethat is not provisioned yet wires onlyvideo-stream/setup(StreamManager::isSetUp()); every other command refuses to run. - A state remembers the provider's scope (
StreamProviderInterface::getScope(), a Bunny library). A database copied to another installation, stage to production for instance, keeps the other library's states: they are neither played, fetched nor deleted, the admin shows them as streamed by another installation, the local file plays meanwhile, andvideo-stream/submit --allstreams them again in this installation's scope (setupsays so when it finds any). A provider cannot copy videos between scopes, so the local files must still exist: never delete the originals on an installation that is copied. - The file grid's thumbnail carries the stream's status as a badge (
Modules\Admin\Widgets\StreamStatusBadge), and the file page shows the status, the provider with the video linked to its backend (getDashboardUrl()), and when it was submitted and turned ready. - The admin plays the stream in the file preview, uses the provider's poster as the grid thumbnail of a video without a preview, and offers "Resubmit stream" in the file's action menu for a stream that is not playable (failed, pending, processing or another installation's): a playable one would play the local file until the new one is ready.
Test\FakeStreamProviderrecords every call and answers configurable states, for tests and a local admin.
Widgets\Media writes the playlist to src (or data-src) with a data-hls attribute and the provider's poster
once the stream is ready, and registers Assets\HlsAssetBundle: Safari plays the playlist natively, every other
browser through hls.js as soon as src is set, whoever sets it. The admin's content security policy admits the
provider's hosts where it restricts media-src, connect-src or img-src; a frontend policy restricting them adds
the hosts the provider's getContentSecurityPolicy() answers to all three, plus blob: to media-src.
Rendering a video
Widgets\Media renders a <video> for an asset whose file answers isVideo() and falls back to the media
widget's picture rendering otherwise, so the frontend calls the media widget as before:
use Hirtz\Media\Widgets\Media; echo Media::make() ->asset($asset) ->autoplay() ->video(fn (Video $video) => $video->addClass('lazyload'));
autoplay()setsautoplay,playsinline,loopandmutedtogether; without it the tag carriescontrolsandpreload(nonewhilelazyLoading()is on,autootherwise) and none of the four.lazyLoading()(on by default) writes the URL todata-srcinstead ofsrc, for a loader script to pick up.aspectRatio(true)writes the file's width and height as anaspect-ratiostyle, as it does for an image.subtitles(false)leaves out the<track>the widget otherwise adds for each language whose subtitles are stored, labelled with the language's own name,defaulton the application language's.video(Closure)is handed theHirtz\Skeleton\Html\Videotag before it renders, for a class or any other attribute the widget does not set itself, aposterover the stream's included.
Admin
Modules\Admin\Widgets\Forms\Fields\FilePreviewFieldshows a<video controls>on the file page instead of the image preview, with the subtitle tracks.Modules\Admin\Widgets\Grids\Columns\Thumbnaildraws a play icon on a blackimg-thumbnailin the file grid, published byAssets\VideoAssetBundlefromresources/video/video.svg.Models\Behaviors\FileVideoBehavioris attached to everyHirtz\Media\Models\Fileand reads the width and height of an uploaded video before validation; a file getID3 cannot analyze fails validation onupload.