
Available sub-templates
1. Assets.html
Registers CSS and JavaScript assets conditionally
<f:render partial="VidPly/Assets" arguments="{
needsPrivacyLayer: vidply.needsPrivacyLayer,
needsVidPlay: vidply.needsVidPlay,
needsPlaylist: vidply.needsPlaylist,
needsHLS: vidply.needsHLS,
needsDASH: vidply.needsDASH
}" />
Arguments:
needsPrivacyLayer- Load PrivacyLayer.js (external services)needsVidPlay- Load VidPly core (native player)needsPlaylist- Load PlaylistInit.js (playlists/player initialisation)needsHLS- Load hls.js (HLS streaming)needsDASH- Load dash.js (DASH streaming)
Always loaded:
vidply.min.css- Player styling
Loaded conditionally depending on media type:
privacy-layer.css- Privacy layer styles (external services only)PrivacyLayer.js- YouTube, Vimeo, SoundCloudhls.min.js— Vendored hls.js 1.7.2 (HLS streams only)dash.all.min.js— Embedded ‘dash.js’ 5.2.1 (modern UMD; DASH streams only)PlaylistInit.js- Playlists or player initialisationvidply/vidply.esm.min.js- Native player only (plus code-split chunks)
See Documentation/AssetLoading.md for details on optimisation.
2. VideoSources.html
Renders ‘<source>’ elements for video players
<f:render partial="VidPly/VideoSources" arguments="{
videoUrl: vidply.videoUrl,
playlistData: vidply.playlistData,
sources: vidply.sources,
mediaFiles: vidply.mediaFiles
}" />
Supports:
- Multiple source formats with fallback order (DASH → HLS → MP4/WebM)
- Multiple quality and format options
- Playlist mode
- Sources for audio descriptions
3. AudioSources.html
Renders ‘<source>’ elements for audio players
<f:render partial="VidPly/AudioSources" arguments="{
playlistData: vidply.playlistData,
tracks: vidply.tracks,
sources: vidply.sources,
mediaFiles: vidply.mediaFiles
}" />
Supported:
- Multiple source formats with fallback order (DASH → HLS → MP3/OGG)
- Playlist mode
- Format fallback
4. Tracks.html
Renders subtitle and chapter<track>elements
<f:render partial="VidPly/Tracks" arguments="{
captions: vidply.captions,
chapters: vidply.chapters,
languageSelection: vidply.languageSelection
}" />
Shared by video and audio players. Supports multiple languages and title types.
5. MetadataScripts.html
JSON payloads for audio description and sign language (read by the player on initialisation – not JSON-LD)
<f:render partial="VidPly/MetadataScripts" arguments="{
uniqueId: vidply.uniqueId,
audioDescriptionTracks: vidply.audioDescriptionTracks,
audioDescription: vidply.audioDescription,
signLanguage: vidply.signLanguage
}" />
Used by video players for metadata relating to audio description and sign language tracks.
SEO JSON-LD (
VideoObject/AudioObject) is rendered separately on the page<head>viaPartials/Page/StructuredDataVideo.html— see Developer Quick Start → Structured Data.
6. PrivacyLayer.html
GDPR consent layer for external services
<f:render partial="VidPly/PrivacyLayer" arguments="{
service: 'youtube',
videoUrl: vidply.videoUrl,
poster: vidply.poster,
title: data.header,
uniqueId: vidply.uniqueId,
privacySettings: vidply.privacySettings.youtube
}" />
Displays a play button with a privacy notice for YouTube, Vimeo and SoundCloud. Uses the database settings from the tx_mpcvidply_privacy_settings table, with a fallback to translations from the language file. Loads an iframe upon consent.
Structure of the privacy settings:
[
'headline' => 'Optional headline',
'intro_text' => 'Text before link',
'outro_text' => 'Text after link',
'policy_link' => 'https://...',
'link_text' => 'Link text',
'button_label' => 'Button aria-label'
]
7. MediaPlayer.html
The ‘<video>’ or ‘<audio>’ element itself
Player.html sets mediaKind to audio or video and renders this
partial for both. mediaKind is also the element name, i.e. the tag that
is used by default aria-label, the poster attribute (video only), the source partials
and MetadataScripts.html (for video only) are all derived from it.
<f:variable name="mediaKind" value="video" />
<f:render partial="VidPly/MediaPlayer" arguments="{_all}" />
It replaces the previous VideoPlayer.html and AudioPlayer.html, which
were identical apart from these five points. Site packages that override either of
these two must MediaPlayer.html override instead.
Template structure
VidPly.html (Main)
├── layout = card|episodes → VidPly/EpisodeLayout.html
│ ├── VidPly/EpisodeCard.html (cover, episode number, title, date, duration, description)
│ └── VidPly/Player.html
└── VidPly/Player.html (wrapper + renderMode switch)
├── privacy → PrivacyLayer.html
├── mixedPlaylist → MixedPlaylistPlayer.html
└── audio / video → MediaPlayer.html (mediaKind = audio|video)
├── VideoSources.html / AudioSources.html
├── Tracks.html
└── MetadataScripts.html (video only)
└── Assets.html (conditional JS/CSS)
At page level: Partials/Page/StructuredDataVideo.html (JSON-LD in <head>).
Customisation
Overriding in the site package
TypoScript:
tt_content.mpc_vidply {
partialRootPaths.100 = EXT:your_sitepackage/Resources/Private/Partials/
}
Create a custom partial:
your_sitepackage/
└── Resources/Private/Partials/VidPly/
└── VideoSources.html ← Your customization
Other partials remain unchanged.
List view and detail templates
Location: Resources/Private/Partials/Listview/ and
Resources/Private/Templates/Listview.html / Detail.html
| Partial / Template | Role |
|---|---|
Listview/Row | A shelf or grid row: Heading, Sorting <select>, optional pagination wrapper |
Listview/RowListBody | Shared <ul> for grid or shelf (can be used with or without pagination) |
Listview/Card | Individual media card (poster, title, artist, category chips, link) |
Listview/ShelfArrows | ‘Back/Next’ controls for the horizontal shelf |
Overwrite in the same way using partialRootPaths for mpc_vidply List view
TypoScript, if your site package requires a different markup; retain data-*
attributes if you use Listview.js (shelf, sorting, pagination).
Sub-templates for episode cards
These are used when the ‘Layout’ field of the content element (tx_mpcvidply_layout) is set
to ‘Episode card’ or ‘Episode card with episode list’; default renders the
simple player exactly as before.
| Part | Role |
|---|---|
VidPly/EpisodeLayout | Layout wrapper, loads episode.min.css + EpisodeInit.min.js, renders the map or the episode list as well as the player |
VidPly/EpisodeList | The <ol> episode rows, which are rendered either separately or within the pager container |
VidPly/EpisodeCover | Square thumbnail image of an episode |
VidPly/EpisodeCard | A round play button next to the episode number, title, release date, duration, description, download link and the icon for viewing the detailed description |
EpisodeCard requires an titleLevel argument (3 for the single card, 4 for list rows
) to ensure the order of the headings below the content element’s header remains unbroken.
The two layouts
Both start with the same card: mpc-episode-cover alongside mpc-episode-main,
which stacks the card text onto the player. The episode card ends at this point and displays
vidply.episodethe first entry.
The episode card with episode list adds a mpc-episode-list-section , which changes to
a full-width line below it and displays each record in vidply.episodes as <ol>
row mpc-episode-item rows. The card then acts as a ‘Now Playing’ header:
EpisodeInit.js Whenever the title changes, the selected row is copied into this card, so that the
cover, title, date, duration, categories and description always correspond to the
current title in the player. Because of this copying, the markup rendered by the server remains the sole
source for the episode data – nothing is reformatted in JavaScript.
As the list is the title overview, the processor disables the player’s own
playlist window (showPanel: false) and hides its toggle button
(playlistToggleButton: false) – otherwise, the same data records
would be displayed twice by two different formatters.
The episode data comes from EpisodeListBuilder::build(), and is layout-specific, so that
the episode card does not query for posters and categories from records that it will never
display. Dates, running times and download sizes
are already formatted in PHP for the website’s language, so that templates output ready-made
strings.
Only records from which a playable track has been created are included in vidply.episodes, which
ensures that episode.index remains usable as a track index in the playlist: a record whose
source cannot be resolved is immediately removed from both lists.
Sorting and pagination of the episode list
The list has its own sort drop-down menu and, from a configurable page size onwards, a
pager – modelled on the list view (Listview.js), which, however, in
EpisodeInit.js.
Both only affect the list. episode.index The playlist’s title index remains unchanged,
regardless of how the rows are sorted or which ones are hidden, so that the player
retains the editor’s order for ‘Next/Previous’ and automatic fast-forwarding. This also means that the
‘Now Playing’ card is the track with the index 0
(EpisodeListBuilder::resolveLeadEpisode()), not as the first array entry.
Field (tt_content) | Effect |
|---|---|
tx_mpcvidply_episode_sort | Default order: sorting (order of media elements), date_desc, date_asc, title_asc. Applied on the server side and inserted into the <select> |
tx_mpcvidply_episode_pagination | Enables pagination; this only becomes visible once there are more episodes than can fit on a single page |
tx_mpcvidply_episode_per_page | Episodes per page, limited to 1–200 (default 10) |
Episodes without a publication date are sorted last in both date modes – both on the server and in the browser – so that an incomplete record never appears at the top of a ‘Newest first’ list. In the event of a tie, the title index is used.
Hidden rows carry the hidden attribute, which removes them from the tab order
and the accessibility tree; the heading retains the full episode count, whilst the
page counter indicates the current page. When changing pages, the focus jumps to the first row of the new
page, and when the title changes, the page with the active row is displayed – this
ensures the list is carried along when scrolling automatically past the end of a page.
| Attribute | Purpose |
|---|---|
data-mpc-episode-default-sort | For the section: the order pre-selected by the editor, which is applied <select> when loading |
data-mpc-episode-sort | The sort order <select> |
data-mpc-episode-list | The <ol> which <li> children are sorted and paginated |
data-mpc-episode-date / data-mpc-episode-sort-title | Sort key per row (Y-m-d, empty if not specified) |
data-mpc-episode-paginate | Pager container; contains data-mpc-episode-per-page as well as the pager labels |
data-mpc-episode-pager-nav | Empty <nav> The buttons appear in |
‘Downloads’ in the episode list
episode.downloadUrl and episode.downloadInfo (“MP3, 7.4 MB”) are populated for
entries with the ‘Allow download’ option – but only in the episode list layout of a
playlist. As every episode is displayed on the page there, one link per line allows visitors to
save any episode without having to select it first.
In any other combination, the download is initiated via the button in the player’s control bar,
which retrieves the file from the selected track in the playlist
(track.downloadUrl / downloadFormat / downloadFileSizesee
DownloadResolver::enrichTrack()). This is also why the episode card
does not display a link: it shows a specific episode, whilst the player may already be on
a different one; therefore, a link rendered by the server would be out of date there.
In any case, exactly one option is offered: buildPlaylistData() only groups
downloadButton: true outside the episode list layout.
The size is measured in terms of storage space required (ResourceStorage::getFileInfo()), not
based on sys_file.size: A file that has been replaced without re-indexing would otherwise
be displayed with the size of its predecessor. By passing the same number of bytes to the player,
both labels remain consistent, and the HEAD request that the button
would otherwise send. If the storage cannot respond, the line label relies on the
format alone and the player measures the file itself.
Within a line, the link position: relative and a z-index above the stretched overlay of the
play button, as otherwise the full-line-width play target would swallow up clicks on it.
The detailed description of a media item
long_description is the medium’s RTE text. The details page displays it in full;
the map and each list row instead present it as a drop-down field, so that a long list of episodes
remains clear and organised. The button appears for records where the field is filled in
– there is no editor toggle for it.
Both halves are rendered by the server: the panel is rendered using the hidden
attribute and EpisodeInit.js only expands hidden, aria-expanded and the
button label. The panel contains no id and the button contains no aria-controls: The
‘Currently Playing’ card is a clone of a list row, so an ID would appear twice in the
document as soon as a track is selected. aria-expanded A button immediately
followed by its panel expresses the same relationship without this
risk, which is why the panel is also searched for within the
mpc-episode-body rather than via a reference.
| Attribute | Purpose |
|---|---|
data-mpc-episode-longdesc-toggle | The button for showing and hiding. Contains data-mpc-episode-label-more / -less for both states and data-mpc-episode-title for the accessible name |
data-mpc-episode-longdesc | The panel controlled by the hidden attribute |
Within a single line, the button and the area require a z-index above the ‘Play’
overlay. Without this, the line-wide ‘Play’ target will capture the clicks, and the
expanded text cannot be selected or clicked as a link.
If you override the card, retain these hooks – EpisodeInit.js relies on them:
| Attribute | Purpose |
|---|---|
data-mpc-episode-play | Identifies the play button. Clicks are delegated so that a cloned button works without reinitialisation. Within a row ::after spans the entire row – the same trick as with Bootstrap .stretched-link in the teasers, which is why the line position: relative |
data-mpc-episode-index | playlist title selected by the button (playlistManager.play(index)) |
data-mpc-episode-item | row wrapper. Remains aria-current="true" as long as its title is selected and serves as the cloning source for the card |
data-mpc-episode-current | At the root: an index of the title currently displayed by the card. Is rendered as 0 , so that nothing needs to be replaced on first render |
Class names follow the Bootstrap pattern (mpc-episode, mpc-episode-cover,
mpc-episode-body, mpc-episode-play, …) with the layout as a modifier on the
root element (mpc-episode-layout-card, mpc-episode-layout-episodes). The
image assets are located in mpc-episode-cover and everything else in mpc-episode-main, so that
the player is aligned with the play button rather than the image. List items reduce
the size of the cover and the play bubble by using --mpc-vidply-episode-cover and
--mpc-vidply-episode-play-size, which is why a row cloned into the card
is displayed at full size.
The heading levels differ between the two positions – titleLevel 3 for the card, 4
for rows — so the clone takes on the tag of the title it replaces, rather than
extending the row <h4> up to the top edge of the element.
Two things to bear in mind when customising the design of a row: the play button must not have a
transform, as a transformed element becomes the enclosing block of its
own overlay and the clickable area would jump back to the bubble on hover (instead, the
hover and :focus-within states of the row take over the affordance instead). And the overlay
covers the text of the row, so it is not possible to select a row’s short description with the mouse
– the same compromise that the teasers also make. The expanded long
description is the exception: it is raised above the overlay and remains
selectable.
Advantages
- Single point of responsibility – Each sub-element has a clear purpose
- Reusability – Tracks sub-elements shared by video and audio
- Easy to overwrite – Adjust only what you need
- Maintainability – Changes are limited to specific files
- Readability – The main template has been reduced from 250 to 130 lines