Read from the package's own PHP files at the commit e560817678d2: its namespaces, classes, interfaces and enums, their public constants, properties and methods with their PHPDoc, and the options its factories take. Nothing of it is run.
VideoBundleclassA video platform on top of omnibase's Thread: a Video IS a thread (a JOINED subclass, so it has its title, text, slug, owners, tags, likes, publication states and trash); a channel, a source, a playlist, a watch and a subscription are plain rows. A comment is omnibase's own, with the moment of the film it was written at.
3 methods
GorseKeySectionclassGorse's server key (video.suggest.provider: gorse), on the API keys page: typed there it wins over the configuration.
3 methods
VideoOverviewWidgetTypeclassThe platform at a glance: comments waiting, reports open, films being transcoded or failed, views of the last thirty days. Placed with `yield MenuItem::block('video_overview', ...)` in the dashboard.
4 methods
CleanUploadsCommandclasstusd's leftovers: uploads started and never finished, and finished ones no video claims, older than --days. The ones still waiting for their transcoding are kept.
2 methods
FlushViewsCommandclassThe views counted since the last run, added to Video::$views - from cron, every few minutes.
2 methods
RelatedCommandclassThe suggestions' Related table computed again - from cron, at night.
2 methods
TranscodeCommandclassOne film transcoded here and now, from a file: a back-office repair, an import.
2 methods
OpenToEditorsTraittraitThe platform's screens are written by its staff - the administrators (catalogue) and, for moderation, the moderators (ROLE_EDITOR) - not only by the super-administrator omnibase/admin requires by default for anything that writes. $custom names the screen's own actions.
Base\Video\Controller\Admin\Crud
ChannelCrudControllerclassThe channels: their names and addresses, their owners, how many follow them.
4 methods
VideoCommentCrudControllerclassModerating the comments under the films: what waits, what is online, what Akismet held - approve, spam, trash. The moderators' screen (ROLE_EDITOR); comments are written under the films, never here.
8 methods
VideoCrudControllerclassThe films, all of them: their state and processing, who sent them, their figures; "retirer" takes one down with a reason its authors are told, "rétablir" puts it back (private). Written by the administrators; the moderators take down and restore.
8 methods
ChannelControllerclassA channel's page - its shelves, its videos, its lists, what it says - and following it.
3 methods · 1 constant
LibraryControllerclassWhat a member keeps: the history, "plus tard", the lists, the subscriptions' feed - and a list's own page, for anyone it is shown to.
10 methods
SearchControllerclassThe search: one bar, the platform's films (Typesense, the database when it is down), its filters.
2 methods
StudioControllerclassA member's studio: pages of the site (not a second back office) where they send films, fill them in, choose who sees them and when, read their figures, look after their comments, their channel, their trash.
14 methods
UploadControllerclasstusd's hooks (pre-create, post-finish). Called from inside (tusd -> web:80); the proxy refuses this address from outside. Whatever it is sent, the answer is decided by the upload token (Service\Uploads), never by who calls.
1 method
WatchControllerclassA film's page and what it answers: the manifest of a remote film, the player's beacons, the ratings, the comments, the reports.
8 methods
VideoDemoAccountsclassThe demonstration accounts of a video platform (glitchr/omnibase's `demo` environment): one for each of its people. A creator and a member hold no role - every member has the studio (ROLE_USER); what tells them apart is what the fixtures give them: a channel, its films and their statistics to the first, subscriptions, a list and comments to the second. The moderators are ROLE_EDITOR (VideoVoter::MODERATE: the comments, the reports, the take-downs), the catalogue is ROLE_ADMIN's (every film and channel, in the back office).
2 methods
Base\Video\DependencyInjection
VideoConfigurationclassconfig/packages/video.yaml. Every leaf is also a flat parameter (video.upload.enabled, video.suggest.provider...): docs/configuration.md.
1 method
VideoExtensionclass3 methods
ChannelclassA member's channel - or a broadcaster's: its name, its address, its banner and picture, what it says of itself, the shelves its page shows, its subscribers. A member gets one the first time they publish; owners share it the way omnibase's threads share theirs (several accounts).
26 methods · 1 constant
PlaylistclassA list of videos a member keeps: their own lists (public, unlisted or private) and "À regarder plus tard" (one a member, always private). A channel shows its owners' public lists. The history is not a list: it is the member's WatchEvents.
27 methods · 6 constants
PlaylistItemclass7 methods
Relatedclass"À suivre": a video's neighbours and how close they are, computed at night by Suggest\RelatedCalculator (video:related). Read by LocalSuggest.
6 methods
SourceclassWhere a video plays from. One MASTER a video (the address the player is given) and, for our own films, one RENDITION per rung of the ladder:
23 methods · 7 constants
SubscriptionclassA member following a channel: its new videos in their feed (/abonnements).
5 methods
VideoclassA film. An omnibase thread - its title, its description (the content), its slug, its owners, its tags (the keywords), its likes, its states and its trash - and what a film adds: the channel it is on, where it plays from (its Sources: our own HLS, a remote HLS read without a copy, or a platform's player), how long it is, its poster and storyboard, where its processing stands, how many times it was seen.
78 methods · 10 constants
VideoCommentclassomnibase's comment (Base\Entity\Thread\Comment: its states, its replies on one level, its Akismet score) with what a film adds: the moment it was written at - where the player stood - and whether its author pinned it.
8 methods · 1 constant
WatchEventclassOne sitting in front of a film: who (an account, or a visitor's hashed id), from where (home, search, channel, playlist, suggestion, another site), how far the player went and how many seconds were really watched.
19 methods · 1 constant
RemoteExceptionclassA remote playlist that cannot be read, or is not allowed (video.remote.hosts).
TranscodingExceptionclassffmpeg or ffprobe missing, failing, or a film it cannot read.
1 method
ChannelDetailsTypeclass2 methods
LinkDetailsTypeclass2 methods
PlaylistDetailsTypeclass2 methods
VideoDetailsTypeclassThe studio's form for one film (on Model\VideoDetails).
3 methods
ComputeRelatedclassThe suggestions' Related table computed again (video:related --async, at night).
FlushViewsclassThe views counted since the last flush, added to Video::$views (video:views:flush --async, from cron).
SuggestFeedbackclassWhat a viewer did, for Gorse (video.suggest.provider: gorse): read (a playback started), watch (a view counted), like, or a negative "not interested" - sent through Messenger so a slow Gorse never slows a page.
1 method · 4 properties
TranscodeVideoclassA film to transcode: sent by the upload's post-finish hook (or the replacement of a file), handled by the worker. Route it to an async transport (framework.messenger.routing).
1 method · 2 properties
ComputeRelatedHandlerclass2 methods
FlushViewsHandlerclass2 methods
SuggestFeedbackHandlerclassHands a viewer's gesture to Gorse; a Gorse that is down loses it, nothing else does.
2 methods
TranscodeVideoHandlerclassTranscodes an uploaded film (Service\Processing::transcode()).
2 methods
ChannelDetailsclass1 method · 6 properties
LinkDetailsclassA film of another platform, added by its address (framed through embed_url()).
2 properties
PlaylistDetailsclass1 method · 3 properties
VideoDetailsclassWhat the studio's form edits of a film (omnibase's FormFactory takes no entity as form data): its words, its filing, who sees it and when, its comments, its poster.
2 methods · 11 properties
ChannelRepositoryclass4 methods
PlaylistRepositoryclass6 methods
RelatedRepositoryclass5 methods
SourceRepositoryclass1 method
SubscriptionRepositoryclass5 methods
VideoCommentRepositoryclass6 methods
VideoRepositoryclass18 methods
WatchEventRepositoryclass13 methods
SafeIndexerclassglitchr/typesense-bundle's indexer, made safe for a search server that is down: whatever the client throws (the bundle only catches its own and php-http's exceptions - a PSR-18 network error from symfony/http-client went through and failed the page that saved a film), the index misses a change, the save does not fail and no visitor is told about Typesense.
2 methods
VideoSearchclassThe platform's search: Typesense (glitchr/typesense-bundle, the "video" mapping of the site's config/packages/typesense.yaml - docs/search.md) with typo tolerance and prefixes, filtered by length, date, category and channel, the listed films only. With no such mapping, or the server down, the database answers (every word in the title, the text, the channel).
3 methods · 1 constant
VideoVoterclassVIDEO_VIEW a film reachable (listed or unlisted), or its owners', or the moderators' VIDEO_EDIT its owners (and its channel's), or an administrator VIDEO_MODERATE the moderators (ROLE_EDITOR and up): take down, hide a comment Also on a Channel (VIDEO_EDIT: its owners) and a Playlist (VIDEO_VIEW, VIDEO_EDIT).
1 method · 3 constants
ChannelsclassA member's channel, made the first time they publish (named after them, renamed in the studio), and the subscriptions to channels, whose count the channel keeps.
4 methods
CommentsclassA comment left under a film: omnibase's comment (its form, its guard, Akismet's score) made a VideoComment with the moment of the film it was written at, a reply attached to its comment (one level), the video's authors told. A video whose comments are closed takes none.
4 methods
EditorclassWhat the studio writes: a film's details applied (the keywords made omnibase tags, the poster a still or a picture sent), a film of another platform added by its address.
4 methods
ModerationclassReports and take-downs. A report is omnibase's Complaint (source "video"), handled on omnibase/admin's screen for them: what is reported (a video, a comment, a channel) is its location, its author the target.
5 methods · 1 constant
NotifierclassWhat a video's authors are told, in the bell of omnibase's notification centre ('notify', in-app): their film is ready, or failed; somebody commented it; the moderators took it down, and why. A notification that cannot be sent (no notifier in a test, a mailer down) never stops what caused it.
5 methods
PicturesclassThe pictures a member sends in the studio - a poster of their own, a channel's picture and banner - kept with the films (served by the same address), under random names.
3 methods · 1 constant
ProcessingclassA film's way from the uploaded file to what plays: running, the ladder built beside the old one (a replacement keeps playing until the new one is whole), the sources, the poster unless its author chose one, the storyboard, ready - or failed, with why. The original is removed once the ladder exists (video.upload.keep_originals: 0).
4 methods
Ratingsclass"J'aime" and "je n'aime pas", on omnibase's own Like (Base\Entity\Thread\Like, on any thread): a thumbs-up is the core's icon, a thumbs-down another icon on the same row. One rating a member and film; choosing the other one turns it over, choosing the same one again takes it back. The thread's row is locked while it is decided (a double click made two rows).
4 methods · 2 constants
RemoteManifestclassSomeone else's HLS, played without a copy. A master playlist that its origin serves without CORS is read here, kept a while (video.remote.ttl) and given to the browser from our address, its URIs made absolute: the variant playlists and the segments are then fetched by the browser from the origin itself, which allows it. Nothing else of the film passes through the site.
8 methods
StorageclassWhere a video's files live: one directory a video under video.storage, named by a random key (a private film's files cannot be guessed), served by the web server at video.public_path. What the entities keep are the public addresses (/media/videos/<key>/master.m3u8), so the pages need no service to show a poster.
10 methods
TranscoderclassA film turned into what the player reads, with ffmpeg (symfony/process):
14 methods · 3 constants
UploadsclassThe resumable uploads, with tusd. The studio hands the browser a signed token (who, until when, and - for a replacement - which video); Uppy sends it in the upload's metadata; tusd asks the site twice:
10 methods
ViewsclassThe player's beacons, and the rule that makes a sitting a view: once min(30 s, half the film) has really been played (video.views.min_seconds, .ratio), and only once per visitor and film every six hours (video.views.dedupe) - the visitor being the account, or a hash of the address and the browser that never leaves this service. Counted views are added to Video::$views by flush() (video:views:flush, every few minutes from cron), not one UPDATE per beacon.
6 methods
AffinityInterfaceinterfaceWhat else makes two films close, from another bundle: omnibase/broadcast says "same artist" (as strong as the same channel) and "same episode" (as strong as the same list). The calculator adds, for a pair, the weight of each strength they share once: same channel and same artist is 3, not 6.
1 method
GorseSuggestclassGorse (gorse-in-one, Apache-2.0), spoken to over its REST API with symfony/http-client - no client library:
8 methods
LocalSuggestclassThe suggestions computed here: a film's neighbours from the Related table (RelatedCalculator, at night); for a member or a visitor, the neighbours of what they watched last, summed, what they already saw left out, and one in five taken from the trends so the feed is not only more of the same. No more than video.suggest.per_channel films of one channel in a list.
6 methods
RelatedCalculatorclassThe Related table, computed again at night (video:related). For two listed films A and B, B's score after A:
5 methods · 2 constants
SuggestInterfaceinterfaceWhat to watch next. Two implementations: LocalSuggest (always there: the Related table computed at night, the member's history, the trends) and GorseSuggest (gorse-in-one, optional). Suggestions picks the configured one and falls back to the local one: Gorse off or down, nothing breaks.
4 methods
SuggestionsclassThe suggestions the pages ask for (SuggestInterface is this): Gorse when video.suggest.provider is "gorse" and it answers enough, the local ones otherwise - and to fill a short answer. A visitor's own films never come back to them.
6 methods
VideoExtensionclassThe pages' words for films:
10 methods
Base\Video
VideoBundle class
class VideoBundle extends AbstractBaseBundle VideoBundle.php:16
A video platform on top of omnibase's Thread: a Video IS a thread (a JOINED subclass, so it has its title, text, slug, owners, tags, likes, publication states and trash); a channel, a source, a playlist, a watch and a subscription are plain rows. A comment is omnibase's own, with the moment of the film it was written at.
VideoBundle::__construct()
public function __construct()VideoBundle::getPath()
public function getPath(): stringModern layout: the class lives in src/, the bundle root is the package root.
VideoBundle::build()
public function build(ContainerBuilder $container): voidBase\Video\Admin\Settings
GorseKeySection class
final class GorseKeySection implements SettingsSectionInterface GorseKeySection.php:10
Gorse's server key (video.suggest.provider: gorse), on the API keys page: typed there it wins over the configuration.
GorseKeySection::__construct()
public function __construct(readonly TranslatorInterface $translator)GorseKeySection::getPage()
public function getPage(): stringGorseKeySection::getFields()
public function getFields(): arrayBase\Video\Admin\Widget
VideoOverviewWidgetType class
final class VideoOverviewWidgetType implements DashboardWidgetTypeInterface VideoOverviewWidgetType.php:19
The platform at a glance: comments waiting, reports open, films being
transcoded or failed, views of the last thirty days. Placed with
yield MenuItem::block('video_overview', ...) in the dashboard.
VideoOverviewWidgetType::__construct()
public function __construct(readonly VideoRepository $videos, readonly VideoCommentRepository $comments, readonly WatchEventRepository $events, readonly EntityManagerInterface $entityManager)VideoOverviewWidgetType::getName()
public static function getName(): stringVideoOverviewWidgetType::getTemplate()
public function getTemplate(): stringVideoOverviewWidgetType::getTemplateVars()
public function getTemplateVars(MenuItem $widget): arrayBase\Video\Command
CleanUploadsCommand class
final class CleanUploadsCommand CleanUploadsCommand.php:18
tusd's leftovers: uploads started and never finished, and finished ones no video claims, older than --days. The ones still waiting for their transcoding are kept.
CleanUploadsCommand::__construct()
public function __construct(readonly VideoRepository $videos, readonly string $directory)CleanUploadsCommand::__invoke()
public function __invoke(SymfonyStyle $io, int $days = 3, bool $dryRun = false): intFlushViewsCommand class
final class FlushViewsCommand FlushViewsCommand.php:14
The views counted since the last run, added to Video::$views - from cron, every few minutes.
FlushViewsCommand::__construct()
public function __construct(readonly Views $views, readonly MessageBusInterface $bus)FlushViewsCommand::__invoke()
public function __invoke(SymfonyStyle $io, bool $async = false): intRelatedCommand class
final class RelatedCommand RelatedCommand.php:14
The suggestions' Related table computed again - from cron, at night.
RelatedCommand::__construct()
public function __construct(readonly RelatedCalculator $calculator, readonly MessageBusInterface $bus)RelatedCommand::__invoke()
public function __invoke(SymfonyStyle $io, bool $async = false): intTranscodeCommand class
final class TranscodeCommand TranscodeCommand.php:13
One film transcoded here and now, from a file: a back-office repair, an import.
TranscodeCommand::__construct()
public function __construct(readonly VideoRepository $videos, readonly Processing $processing)TranscodeCommand::__invoke()
public function __invoke(SymfonyStyle $io, string $video, string $file): intBase\Video\Controller\Admin
OpenToEditorsTrait trait
trait OpenToEditorsTrait OpenToEditorsTrait.php:14
The platform's screens are written by its staff - the administrators (catalogue) and, for moderation, the moderators (ROLE_EDITOR) - not only by the super-administrator omnibase/admin requires by default for anything that writes. $custom names the screen's own actions.
Base\Video\Controller\Admin\Crud
ChannelCrudController class
class ChannelCrudController extends AbstractCrudController ChannelCrudController.php:17
The channels: their names and addresses, their owners, how many follow them.
ChannelCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringChannelCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringChannelCrudController::configureFields()
public function configureFields(string $pageName): iterableChannelCrudController::configureActions()
public function configureActions(Actions $actions): ActionsVideoCommentCrudController class
class VideoCommentCrudController extends AbstractCrudController VideoCommentCrudController.php:27
Moderating the comments under the films: what waits, what is online, what Akismet held - approve, spam, trash. The moderators' screen (ROLE_EDITOR); comments are written under the films, never here.
VideoCommentCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringVideoCommentCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringVideoCommentCrudController::configureFilters()
public function configureFilters(Filters $filters): FiltersVideoCommentCrudController::configureFields()
public function configureFields(string $pageName): iterableVideoCommentCrudController::configureActions()
public function configureActions(Actions $actions): ActionsVideoCommentCrudController::approve()
public function approve(string $entityId): ResponseVideoCommentCrudController::spam()
public function spam(string $entityId): ResponseVideoCommentCrudController::trash()
public function trash(string $entityId): ResponseVideoCrudController class
class VideoCrudController extends AbstractCrudController VideoCrudController.php:34
The films, all of them: their state and processing, who sent them, their figures; "retirer" takes one down with a reason its authors are told, "rétablir" puts it back (private). Written by the administrators; the moderators take down and restore.
VideoCrudController::setVideoServices()
public function setVideoServices(Moderation $moderation): voidVideoCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringVideoCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringVideoCrudController::configureFilters()
public function configureFilters(Filters $filters): FiltersVideoCrudController::configureFields()
public function configureFields(string $pageName): iterableVideoCrudController::configureActions()
public function configureActions(Actions $actions): ActionsVideoCrudController::takedown()
public function takedown(Request $request, string $entityId): ResponseA reason first (the DSA's statement of reasons, sent to the authors), then down.
VideoCrudController::restore()
public function restore(string $entityId): ResponseBase\Video\Controller\Client
ChannelController class
class ChannelController extends AbstractController ChannelController.php:19
A channel's page - its shelves, its videos, its lists, what it says - and following it.
const PER_PAGE = 24
ChannelController::__construct()
public function __construct(readonly ChannelRepository $channels, readonly VideoRepository $videos, readonly Channels $subscriptions)ChannelController::show()
public function show(Request $request, string $slug, PlaylistRepository $playlists): ResponseChannelController::subscribe()
public function subscribe(Request $request, string $slug): ResponseLibraryController class
class LibraryController extends AbstractController LibraryController.php:27
What a member keeps: the history, "plus tard", the lists, the subscriptions' feed - and a list's own page, for anyone it is shown to.
LibraryController::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly PlaylistRepository $playlists, readonly VideoRepository $videos)LibraryController::history()
public function history(WatchEventRepository $events): ResponseLibraryController::clearHistory()
public function clearHistory(Request $request, WatchEventRepository $events): ResponseLibraryController::later()
public function later(): ResponseLibraryController::subscriptions()
public function subscriptions(SubscriptionRepository $subscriptions): ResponseLibraryController::playlists()
public function playlists(Request $request): ResponseLibraryController::playlist()
public function playlist(Request $request, string $slug): ResponseLibraryController::toggle()
public function toggle(Request $request): ResponseAdd a film to a list (or "plus tard"), or take it out: the list's slug, or "later".
LibraryController::delete()
public function delete(Request $request, string $slug): ResponseLibraryController::order()
public function order(Request $request, string $slug): ResponseSearchController class
class SearchController extends AbstractController SearchController.php:14
The search: one bar, the platform's films (Typesense, the database when it is down), its filters.
SearchController::__construct()
public function __construct(readonly VideoSearch $search, readonly array $categories = [])SearchController::search()
public function search(Request $request, ChannelRepository $channels): ResponseStudioController class
class StudioController extends AbstractController StudioController.php:43
A member's studio: pages of the site (not a second back office) where they send films, fill them in, choose who sees them and when, read their figures, look after their comments, their channel, their trash.
StudioController::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly VideoRepository $videos, readonly Uploads $uploads, readonly Channels $channels, readonly string $endpoint = '/files/', readonly int $maxSize = 0, readonly int $quota = 0)StudioController::index()
public function index(Request $request, VideoCommentRepository $comments): ResponseStudioController::upload()
public function upload(): ResponseStudioController::uploaded()
public function uploaded(string $uploadId): ResponseWhere the uploader goes once a file is whole: the film's page in the studio, as soon as the hook made it.
StudioController::link()
public function link(Request $request, Editor $editor): ResponseStudioController::edit()
public function edit(Request $request, int $id, Editor $editor, Processing $processing): ResponseStudioController::status()
public function status(int $id, TranslatorInterface $translator): JsonResponseStudioController::stats()
public function stats(int $id, WatchEventRepository $events): ResponseStudioController::comments()
public function comments(Request $request, int $id, VideoCommentRepository $comments, Comments $service): ResponseStudioController::replace()
public function replace(int $id): ResponseStudioController::delete()
public function delete(Request $request, int $id): ResponseTo the trash (omnibase's TrashBall): back from the studio's trash within the delay, gone after.
StudioController::trash()
public function trash(Request $request, TrashManager $trash): ResponseStudioController::channel()
public function channel(Request $request, ChannelRepository $channels, Pictures $pictures): ResponseStudioController::subscribers()
public function subscribers(SubscriptionRepository $subscriptions): ResponseUploadController class
class UploadController extends AbstractController UploadController.php:18
tusd's hooks (pre-create, post-finish). Called from inside (tusd -> web:80); the proxy refuses this address from outside. Whatever it is sent, the answer is decided by the upload token (Service\Uploads), never by who calls.
UploadController::hook()
public function hook(Request $request, Uploads $uploads): ResponseWatchController class
class WatchController extends AbstractController WatchController.php:40
A film's page and what it answers: the manifest of a remote film, the player's beacons, the ratings, the comments, the reports.
WatchController::__construct()
public function __construct(readonly VideoRepository $videos, readonly VideoCommentRepository $comments, readonly SuggestInterface $suggest, readonly Ratings $ratings, readonly Views $views, readonly Channels $channels, readonly FormFactoryInterface $forms, readonly bool $guests = false)WatchController::watch()
public function watch(Request $request, string $slug, WatchEventRepository $events, PlaylistRepository $playlists): ResponseWatchController::manifest()
public function manifest(string $slug, RemoteManifest $remote): ResponseA remote film's master playlist, from our address (its origin serves it without CORS): read on the server, kept a while, URIs absolute - the browser takes the variants and the segments from the origin.
WatchController::beacon()
public function beacon(Request $request, string $slug): ResponseThe player's beacon: start, progress, end (Service\Views).
WatchController::rate()
public function rate(Request $request, string $slug, RateLimiterFactoryInterface $limiter): ResponseWatchController::comment()
public function comment(Request $request, string $slug, Comments $comments, CommentGuard $guard, RateLimiterFactoryInterface $limiter): ResponseWatchController::report()
public function report(Request $request, string $slug, Moderation $moderation, RateLimiterFactoryInterface $limiter): ResponseWatchController::source()
public static function source(Request $request): stringWhere the visitor came from, for the studio's statistics: the page before, by its address.
Base\Video\Demo
VideoDemoAccounts class
final class VideoDemoAccounts implements DemoAccountProviderInterface VideoDemoAccounts.php:33
The demonstration accounts of a video platform (glitchr/omnibase's demo
environment): one for each of its people. A creator and a member hold no
role - every member has the studio (ROLE_USER); what tells them apart is
what the fixtures give them: a channel, its films and their statistics to
the first, subscriptions, a list and comments to the second. The
moderators are ROLE_EDITOR (VideoVoter::MODERATE: the comments, the
reports, the take-downs), the catalogue is ROLE_ADMIN's (every film and
channel, in the back office).
In an application whose ROLE_EDITOR stands above its super-administrator (omnibase's usual hierarchy, where the editors are the site's makers) the moderator is left out: a super-administrator is never a demonstration account, and the registry would refuse the declaration.
An application's fixtures take them from Base\Demo\DemoAccountFactory ($accounts->account('createur', $manager)) and attach what makes them worth signing in as. A platform without one of them leaves it out: base.demo.exclude.
Registered when the installed glitchr/omnibase has the demo environment (config/services.php).
VideoDemoAccounts::__construct()
public function __construct(readonly ?RoleHierarchyInterface $roleHierarchy = null)VideoDemoAccounts::getDemoAccounts()
public function getDemoAccounts(): iterableBase\Video\DependencyInjection
VideoConfiguration class
class VideoConfiguration extends AbstractBaseConfiguration VideoConfiguration.php:12
config/packages/video.yaml. Every leaf is also a flat parameter (video.upload.enabled, video.suggest.provider...): docs/configuration.md.
VideoConfiguration::getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilderVideoExtension class
class VideoExtension extends AbstractBaseExtension implements PrependExtensionInterface VideoExtension.php:12
VideoExtension::getConfiguration()
public function getConfiguration(array $config, ContainerBuilder $container): VideoConfigurationVideoExtension::prepend()
public function prepend(ContainerBuilder $container): voidThe limiters a member meets (symfony/rate-limiter): comments, ratings, uploads, reports - an application redefines any of them under the same name in its framework.rate_limiter -, and the bundle's cache pool.
VideoExtension::load()
public function load(array $configs, ContainerBuilder $container): voidBase\Video\Entity
Channel class
class Channel Channel.php:20
A member's channel - or a broadcaster's: its name, its address, its banner and picture, what it says of itself, the shelves its page shows, its subscribers. A member gets one the first time they publish; owners share it the way omnibase's threads share theirs (several accounts).
const SHELVES = ['latest', 'popular', 'playlists']The shelves a channel page can show, in the order it shows them.
Channel::__construct()
public function __construct(?string $name = null, ?User $owner = null, ?string $slug = null)Channel::slugify()
public static function slugify(string $text): stringChannel::__toString()
public function __toString(): stringChannel::getId()
public function getId(): ?intChannel::getSlug()
public function getSlug(): ?stringChannel::setSlug()
public function setSlug(?string $slug): selfChannel::getName()
public function getName(): ?stringChannel::setName()
public function setName(?string $name): selfChannel::getDescription()
public function getDescription(): ?stringChannel::setDescription()
public function setDescription(?string $description): selfChannel::getOwners()
public function getOwners(): CollectionChannel::getOwner()
public function getOwner(): ?UserChannel::addOwner()
public function addOwner(User $owner): selfChannel::removeOwner()
public function removeOwner(User $owner): selfChannel::isOwnedBy()
public function isOwnedBy(?User $user): boolChannel::getAvatar()
public function getAvatar(): ?stringChannel::setAvatar()
public function setAvatar(?string $avatar): selfChannel::getBanner()
public function getBanner(): ?stringChannel::setBanner()
public function setBanner(?string $banner): selfChannel::getShelves()
public function getShelves(): arrayChannel::setShelves()
public function setShelves(array $shelves): selfChannel::getSubscribers()
public function getSubscribers(): intChannel::setSubscribers()
public function setSubscribers(int $subscribers): selfChannel::getCreatedAt()
public function getCreatedAt(): ?\DateTimeInterfaceChannel::getVideos()
public function getVideos(): CollectionChannel::getInitials()
public function getInitials(): stringTwo letters for a channel without a picture.
Playlist class
class Playlist Playlist.php:18
A list of videos a member keeps: their own lists (public, unlisted or private) and "À regarder plus tard" (one a member, always private). A channel shows its owners' public lists. The history is not a list: it is the member's WatchEvents.
const LIST = 'list'const LATER = 'later'const PUBLIC = 'public'const UNLISTED = 'unlisted'const PRIVATE = 'private'const VISIBILITIES = [self::PUBLIC, self::UNLISTED, self::PRIVATE]
Playlist::__construct()
public function __construct(User $owner, ?string $title = null, string $kind = self::LIST)Playlist::__toString()
public function __toString(): stringPlaylist::getId()
public function getId(): ?intPlaylist::getSlug()
public function getSlug(): stringPlaylist::getTitle()
public function getTitle(): ?stringPlaylist::setTitle()
public function setTitle(?string $title): selfPlaylist::getDescription()
public function getDescription(): ?stringPlaylist::setDescription()
public function setDescription(?string $description): selfPlaylist::getOwner()
public function getOwner(): UserPlaylist::isOwnedBy()
public function isOwnedBy(?User $user): boolPlaylist::getKind()
public function getKind(): stringPlaylist::isLater()
public function isLater(): boolPlaylist::getVisibility()
public function getVisibility(): stringPlaylist::setVisibility()
public function setVisibility(string $visibility): selfPlaylist::isPublic()
public function isPublic(): boolPlaylist::isReachableBy()
public function isReachableBy(?User $user): boolPlaylist::getItems()
public function getItems(): CollectionPlaylist::getVideos()
public function getVideos(): array- Returns
the videos still reachable, in order
Playlist::has()
public function has(Video $video): boolPlaylist::find()
public function find(Video $video): ?PlaylistItemPlaylist::add()
public function add(Video $video): selfAt the end ("plus tard": at the start, the newest first).
Playlist::remove()
public function remove(Video $video): selfPlaylist::reorder()
public function reorder(array $videoIds): self$videoIdsthe new order, by video id; the others keep theirs after
Playlist::getFirstVideo()
public function getFirstVideo(): ?VideoPlaylist::next()
public function next(Video $video): ?VideoThe one after $video in the list, for "à suivre".
Playlist::getCreatedAt()
public function getCreatedAt(): \DateTimeInterfacePlaylist::getUpdatedAt()
public function getUpdatedAt(): \DateTimeInterfacePlaylistItem class
class PlaylistItem PlaylistItem.php:7
PlaylistItem::__construct()
public function __construct(Playlist $playlist, Video $video, int $position = 0)PlaylistItem::getId()
public function getId(): ?intPlaylistItem::getPlaylist()
public function getPlaylist(): PlaylistPlaylistItem::getVideo()
public function getVideo(): VideoPlaylistItem::getPosition()
public function getPosition(): intPlaylistItem::setPosition()
public function setPosition(int $position): selfPlaylistItem::getAddedAt()
public function getAddedAt(): \DateTimeInterfaceRelated class
class Related Related.php:12
"À suivre": a video's neighbours and how close they are, computed at night by Suggest\RelatedCalculator (video:related). Read by LocalSuggest.
Related::__construct()
public function __construct(Video $video, Video $related, float $score)Related::getId()
public function getId(): ?intRelated::getVideo()
public function getVideo(): VideoRelated::getRelated()
public function getRelated(): VideoRelated::getScore()
public function getScore(): floatRelated::getComputedAt()
public function getComputedAt(): \DateTimeInterfaceSource class
class Source Source.php:19
Where a video plays from. One MASTER a video (the address the player is given) and, for our own films, one RENDITION per rung of the ladder:
hls our transcoding: a master playlist under the storage mp4 a progressive file (an old film, a fallback) remote-hls someone else's HLS, read without a copy: the manifest route serves its master, the browser fetches the rest there embed a platform's own player (YouTube, Vimeo, PeerTube...): its page address, framed through omnibase's embed_url()
const HLS = 'hls'const MP4 = 'mp4'const REMOTE_HLS = 'remote-hls'const EMBED = 'embed'const KINDS = [self::HLS, self::MP4, self::REMOTE_HLS, self::EMBED]const MASTER = 'master'const RENDITION = 'rendition'
Source::__construct()
public function __construct(string $kind = self::HLS, string $url = '', string $role = self::MASTER)Source::__toString()
public function __toString(): stringSource::getId()
public function getId(): ?intSource::getVideo()
public function getVideo(): ?VideoSource::setVideo()
public function setVideo(?Video $video): selfSource::getKind()
public function getKind(): stringSource::getRole()
public function getRole(): stringSource::getUrl()
public function getUrl(): stringSource::setUrl()
public function setUrl(string $url): selfSource::isAbsolute()
public function isAbsolute(): boolSource::getPlatform()
public function getPlatform(): ?stringSource::setPlatform()
public function setPlatform(?string $platform): selfSource::getExternalId()
public function getExternalId(): ?stringSource::setExternalId()
public function setExternalId(?string $externalId): selfSource::setReference()
public function setReference(?string $platform, ?string $frame): selfThe reference of an outside film: [platform, id], from what embed_url() frames ("https://player.vimeo.com/video/76979871").
Source::getWidth()
public function getWidth(): ?intSource::getHeight()
public function getHeight(): ?intSource::setSize()
public function setSize(?int $width, ?int $height): selfSource::getBitrate()
public function getBitrate(): ?intSource::setBitrate()
public function setBitrate(?int $bitrate): selfSource::getCodecs()
public function getCodecs(): ?stringSource::setCodecs()
public function setCodecs(?string $codecs): selfSource::getLabel()
public function getLabel(): string"1080p", for the quality menu.
Subscription class
class Subscription Subscription.php:10
A member following a channel: its new videos in their feed (/abonnements).
Subscription::__construct()
public function __construct(User $user, Channel $channel)Subscription::getId()
public function getId(): ?intSubscription::getUser()
public function getUser(): UserSubscription::getChannel()
public function getChannel(): ChannelSubscription::getCreatedAt()
public function getCreatedAt(): \DateTimeInterfaceVideo class
class Video extends Thread implements LinkableInterface, TypesenseInterface Video.php:33
A film. An omnibase thread - its title, its description (the content), its slug, its owners, its tags (the keywords), its likes, its states and its trash - and what a film adds: the channel it is on, where it plays from (its Sources: our own HLS, a remote HLS read without a copy, or a platform's player), how long it is, its poster and storyboard, where its processing stands, how many times it was seen.
The visibility is the thread's state: public = PUBLISH, unlisted = SECRET (reachable by its address only), private = DRAFT, scheduled = FUTURE with publishedAt (omnibase's thread:publishable puts it online).
Strings rather than PHP enums for the columns: omnibase's Thread reads its properties through __get, and the uploader rehydrates raw values.
const PROCESSING_NONE = 'none'const PROCESSING_UPLOADED = 'uploaded'const PROCESSING_RUNNING = 'running'const PROCESSING_READY = 'ready'const PROCESSING_FAILED = 'failed'const VISIBILITY_PUBLIC = 'public'const VISIBILITY_UNLISTED = 'unlisted'const VISIBILITY_PRIVATE = 'private'const VISIBILITY_SCHEDULED = 'scheduled'const VISIBILITIES = [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED, self::VISIBILITY_PRIVATE, self::VISIBILITY_SCHEDULED]
Video::__iconizeStatic()
public static function __iconizeStatic(): ?arrayVideo::__toLink()
public function __toLink(array $routeParameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): ?stringVideo::__typesense()
public function __typesense(): ?stringVideo::__construct()
public function __construct(?User $owner = null, ?Thread $parent = null, ?string $title = null, ?string $slug = null)Video::__toString()
public function __toString(): stringVideo::getChannel()
public function getChannel(): ?ChannelVideo::setChannel()
public function setChannel(?Channel $channel): selfVideo::getSources()
public function getSources(): CollectionVideo::addSource()
public function addSource(Source $source): selfVideo::removeSource()
public function removeSource(Source $source): selfVideo::clearSources()
public function clearSources(): selfVideo::getPlayableSource()
public function getPlayableSource(): ?SourceThe source the player starts from: the HLS master (ours or remote), else a progressive file, else an embed.
Video::getRenditions()
public function getRenditions(): array- Returns
the renditions of the ladder, the tallest first
Video::isEmbedded()
public function isEmbedded(): boolVideo::isRemote()
public function isRemote(): boolVideo::isPlayable()
public function isPlayable(): boolVideo::getProcessing()
public function getProcessing(): stringVideo::setProcessing()
public function setProcessing(string $processing): selfVideo::isProcessing()
public function isProcessing(): boolVideo::hasFailed()
public function hasFailed(): boolVideo::getFailure()
public function getFailure(): ?stringVideo::setFailure()
public function setFailure(?string $failure): selfVideo::getDuration()
public function getDuration(): ?intVideo::setDuration()
public function setDuration(?int $duration): selfVideo::getDurationText()
public function getDurationText(): string1:02:03, 4:05, 0:07.
Video::formatTime()
public static function formatTime(?int $seconds): stringVideo::getWidth()
public function getWidth(): ?intVideo::getHeight()
public function getHeight(): ?intVideo::setSize()
public function setSize(?int $width, ?int $height): selfVideo::getRatio()
public function getRatio(): string"16 / 9", for the player's frame before anything loads.
Video::getPoster()
public function getPoster(): ?stringVideo::setPoster()
public function setPoster(?string $poster): selfVideo::getStoryboard()
public function getStoryboard(): ?stringVideo::setStoryboard()
public function setStoryboard(?string $storyboard): selfVideo::getStorageKey()
public function getStorageKey(): ?stringVideo::setStorageKey()
public function setStorageKey(?string $storageKey): selfVideo::getUploadId()
public function getUploadId(): ?stringVideo::setUploadId()
public function setUploadId(?string $uploadId): selfVideo::getUploadSize()
public function getUploadSize(): intVideo::setUploadSize()
public function setUploadSize(?int $uploadSize): selfVideo::getOriginalName()
public function getOriginalName(): ?stringVideo::setOriginalName()
public function setOriginalName(?string $originalName): selfVideo::getViews()
public function getViews(): intVideo::setViews()
public function setViews(int $views): selfVideo::getLegacyViews()
public function getLegacyViews(): ?intVideo::setLegacyViews()
public function setLegacyViews(?int $legacyViews): selfVideo::getTotalViews()
public function getTotalViews(): intOurs and the imported ones: what a card shows.
Video::getCategory()
public function getCategory(): ?stringVideo::setCategory()
public function setCategory(?string $category): selfVideo::getLanguage()
public function getLanguage(): ?stringVideo::setLanguage()
public function setLanguage(?string $language): selfVideo::getLicense()
public function getLicense(): stringVideo::setLicense()
public function setLicense(?string $license): selfVideo::areCommentsEnabled()
public function areCommentsEnabled(): boolVideo::isCommentsEnabled()
public function isCommentsEnabled(): boolVideo::setCommentsEnabled()
public function setCommentsEnabled(bool $commentsEnabled): selfVideo::getExternalUrl()
public function getExternalUrl(): ?stringVideo::setExternalUrl()
public function setExternalUrl(?string $externalUrl): selfVideo::getTakedownReason()
public function getTakedownReason(): ?stringVideo::getTakenDownAt()
public function getTakenDownAt(): ?\DateTimeInterfaceVideo::isTakenDown()
public function isTakenDown(): boolVideo::takeDown()
public function takeDown(string $reason): selfOut of sight, the reason kept for its author; restore() puts it back as it was chosen (private).
Video::restore()
public function restore(): selfVideo::getVisibility()
public function getVisibility(): stringVideo::setVisibility()
public function setVisibility(string $visibility, ?\DateTimeInterface $at = null): selfPublic, unlisted, private, or scheduled at $at (a past or missing date publishes now). A film still processing or taken down keeps its choice for later: setVisibility() is only what its author asks.
Video::isListed()
public function isListed(): boolListed on the platform: in the feeds, the search, the suggestions.
Video::isReachable()
public function isReachable(): boolReachable by its address (listed, or unlisted).
Video::isOwnedBy()
public function isOwnedBy(?User $user): boolVideo::getSearchTitle()
public function getSearchTitle(): stringVideo::getSearchText()
public function getSearchText(): stringVideo::getSearchChannel()
public function getSearchChannel(): stringVideo::getSearchChannelSlug()
public function getSearchChannelSlug(): stringVideo::getSearchCategory()
public function getSearchCategory(): stringVideo::getSearchDuration()
public function getSearchDuration(): intVideo::getSearchViews()
public function getSearchViews(): intVideo::getSearchPublished()
public function getSearchPublished(): intVideo::getSearchListed()
public function getSearchListed(): boolVideo::getSearchTags()
public function getSearchTags(): arrayVideoComment class
class VideoComment extends Comment VideoComment.php:17
omnibase's comment (Base\Entity\Thread\Comment: its states, its replies on one level, its Akismet score) with what a film adds: the moment it was written at - where the player stood - and whether its author pinned it.
The "1:23" typed in a text are made clickable by the |timecodes filter; the moment is the comment's own.
const TIMECODE = '/(?<![\d:])(?:(\d{1,2}):)?([0-5]?\d):([0-5]\d)(?![\d:])/'
VideoComment::getMoment()
public function getMoment(): ?intVideoComment::setMoment()
public function setMoment(?int $moment): staticVideoComment::getMomentText()
public function getMomentText(): stringVideoComment::isPinned()
public function isPinned(): boolVideoComment::setPinned()
public function setPinned(bool $pinned): staticVideoComment::getVideo()
public function getVideo(): ?VideoVideoComment::timecodes()
public static function timecodes(string $text): arrayThe moments typed in a text - 1:23, 01:02:03 - in seconds, in the order they come. Hours only before minutes; minutes and seconds under sixty past the first number.
VideoComment::seconds()
public static function seconds(array $m): int$ma TIMECODE match
WatchEvent class
class WatchEvent WatchEvent.php:17
One sitting in front of a film: who (an account, or a visitor's hashed id), from where (home, search, channel, playlist, suggestion, another site), how far the player went and how many seconds were really watched.
It becomes a view once enough was watched (Service\Views), and is added to Video::$views by the next flush (video:views:flush, from cron). The same rows make a member's history and the studio's statistics.
const SOURCES = ['home', 'search', 'channel', 'playlist', 'suggest', 'subscriptions', 'history', 'external', 'direct']
WatchEvent::__construct()
public function __construct(Video $video, string $token, string $visitor, ?User $user = null, string $source = 'direct', ?string $referrer = null)WatchEvent::getId()
public function getId(): ?intWatchEvent::getVideo()
public function getVideo(): VideoWatchEvent::getUser()
public function getUser(): ?UserWatchEvent::getToken()
public function getToken(): stringWatchEvent::getVisitor()
public function getVisitor(): stringWatchEvent::getSource()
public function getSource(): stringWatchEvent::getReferrer()
public function getReferrer(): ?stringWatchEvent::getPosition()
public function getPosition(): floatWatchEvent::getWatched()
public function getWatched(): floatWatchEvent::progress()
public function progress(float $position, float $watched, ?\DateTimeInterface $now = null): selfWhat a beacon says: where the player is, and how many seconds it has played in all this sitting. Neither goes back; a jump in "watched" larger than the time since the last beacon is a forged one, kept to that time.
WatchEvent::isCounted()
public function isCounted(): boolWatchEvent::count()
public function count(): selfWatchEvent::isFlushed()
public function isFlushed(): boolWatchEvent::flush()
public function flush(): selfWatchEvent::isCompleted()
public function isCompleted(): boolWatchEvent::complete()
public function complete(): selfWatchEvent::getStartedAt()
public function getStartedAt(): \DateTimeInterfaceWatchEvent::getUpdatedAt()
public function getUpdatedAt(): \DateTimeInterfaceBase\Video\Exception
RemoteException class
class RemoteException extends RuntimeException RemoteException.php:6
A remote playlist that cannot be read, or is not allowed (video.remote.hosts).
TranscodingException class
class TranscodingException extends RuntimeException TranscodingException.php:6
ffmpeg or ffprobe missing, failing, or a film it cannot read.
TranscodingException::missing()
public static function missing(string $binary): selfBase\Video\Form
ChannelDetailsType class
class ChannelDetailsType extends AbstractType ChannelDetailsType.php:15
ChannelDetailsType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidChannelDetailsType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidLinkDetailsType class
class LinkDetailsType extends AbstractType LinkDetailsType.php:12
LinkDetailsType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidLinkDetailsType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidPlaylistDetailsType class
class PlaylistDetailsType extends AbstractType PlaylistDetailsType.php:14
PlaylistDetailsType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidPlaylistDetailsType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidVideoDetailsType class
class VideoDetailsType extends AbstractType VideoDetailsType.php:21
The studio's form for one film (on Model\VideoDetails).
VideoDetailsType::__construct()
public function __construct(readonly array $categories = [], readonly array $licenses = [])$categories@param list<string> $licenses
VideoDetailsType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidVideoDetailsType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidBase\Video\Message
ComputeRelated class
final class ComputeRelated ComputeRelated.php:6
The suggestions' Related table computed again (video:related --async, at night).
FlushViews class
final class FlushViews FlushViews.php:6
The views counted since the last flush, added to Video::$views (video:views:flush --async, from cron).
SuggestFeedback class
final class SuggestFeedback SuggestFeedback.php:10
What a viewer did, for Gorse (video.suggest.provider: gorse): read (a playback started), watch (a view counted), like, or a negative "not interested" - sent through Messenger so a slow Gorse never slows a page.
public readonly string $typepublic readonly string $userIdpublic readonly int $videoIdpublic readonly ?\DateTimeImmutable $at
SuggestFeedback::__construct()
public function __construct(public readonly string $type, public readonly string $userId, public readonly int $videoId, public readonly ?\DateTimeImmutable $at = null)TranscodeVideo class
final class TranscodeVideo TranscodeVideo.php:10
A film to transcode: sent by the upload's post-finish hook (or the replacement of a file), handled by the worker. Route it to an async transport (framework.messenger.routing).
public readonly int $videoIdpublic readonly string $source
TranscodeVideo::__construct()
public function __construct(public readonly int $videoId, public readonly string $source)Base\Video\MessageHandler
ComputeRelatedHandler class
final class ComputeRelatedHandler ComputeRelatedHandler.php:9
ComputeRelatedHandler::__construct()
public function __construct(readonly RelatedCalculator $calculator)ComputeRelatedHandler::__invoke()
public function __invoke(ComputeRelated $message): voidFlushViewsHandler class
final class FlushViewsHandler FlushViewsHandler.php:9
FlushViewsHandler::__construct()
public function __construct(readonly Views $views)FlushViewsHandler::__invoke()
public function __invoke(FlushViews $message): voidSuggestFeedbackHandler class
final class SuggestFeedbackHandler SuggestFeedbackHandler.php:10
Hands a viewer's gesture to Gorse; a Gorse that is down loses it, nothing else does.
SuggestFeedbackHandler::__construct()
public function __construct(readonly GorseSuggest $gorse)SuggestFeedbackHandler::__invoke()
public function __invoke(SuggestFeedback $message): voidTranscodeVideoHandler class
final class TranscodeVideoHandler TranscodeVideoHandler.php:11
Transcodes an uploaded film (Service\Processing::transcode()).
TranscodeVideoHandler::__construct()
public function __construct(readonly VideoRepository $videos, readonly Processing $processing)TranscodeVideoHandler::__invoke()
public function __invoke(TranscodeVideo $message): voidBase\Video\Model
ChannelDetails class
class ChannelDetails ChannelDetails.php:9
public ?string $name = nullpublic ?string $slug = nullpublic ?string $description = nullpublic array $shelves = Channel::SHELVESpublic ?UploadedFile $avatarFile = null
ChannelDetails::of()
public static function of(Channel $channel): selfLinkDetails class
class LinkDetails LinkDetails.php:8
A film of another platform, added by its address (framed through embed_url()).
public ?string $url = nullpublic ?string $title = null
PlaylistDetails class
class PlaylistDetails PlaylistDetails.php:8
public ?string $title = nullpublic ?string $description = nullpublic string $visibility = Playlist::PRIVATE
PlaylistDetails::of()
public static function of(Playlist $playlist): selfVideoDetails class
class VideoDetails VideoDetails.php:14
What the studio's form edits of a film (omnibase's FormFactory takes no entity as form data): its words, its filing, who sees it and when, its comments, its poster.
public ?string $title = nullpublic ?string $description = nullpublic ?string $category = nullKeywords, separated by commas.
public ?string $language = nullpublic string $license = 'standard'public string $visibility = Video::VISIBILITY_PRIVATEpublic ?\DateTimeInterface $publishAt = nullpublic bool $commentsEnabled = truepublic ?string $poster = nullOne of the stills, chosen as the poster (its address).
public ?UploadedFile $posterFile = null
VideoDetails::of()
public static function of(Video $video): selfVideoDetails::keywords()
public function keywords(): array- Returns
the keywords, trimmed, once each, ten at most
Base\Video\Repository
ChannelRepository class
class ChannelRepository extends ServiceEntityRepository ChannelRepository.php:11
ChannelRepository::__construct()
public function __construct(ManagerRegistry $registry)ChannelRepository::findOneOwnedBy()
public function findOneOwnedBy(User $user): ?ChannelThe first channel a member owns.
ChannelRepository::freeSlug()
public function freeSlug(string $name): stringA free address for $name: "ma-chaine", then "ma-chaine-2".
..
ChannelRepository::findPopular()
public function findPopular(int $limit = 12): array- Returns
the most followed
PlaylistRepository class
class PlaylistRepository extends ServiceEntityRepository PlaylistRepository.php:12
PlaylistRepository::__construct()
public function __construct(ManagerRegistry $registry)PlaylistRepository::findLater()
public function findLater(User $user, bool $create = true): ?Playlist"À regarder plus tard", made the first time it is asked for.
PlaylistRepository::findOwnedBy()
public function findOwnedBy(User $user): array- Returns
a member's own lists (not "plus tard")
PlaylistRepository::findPublicOf()
public function findPublicOf(iterable $owners, int $limit = 24): array- Returns
the public lists of these owners, for their channel page
PlaylistRepository::findPublicIdsContaining()
public function findPublicIdsContaining(Video $video): array- Returns
ids of the public lists a video is in (the suggestions' "same list")
PlaylistRepository::findPublicMemberships()
public function findPublicMemberships(): array- Returns
public list id => its video ids
RelatedRepository class
class RelatedRepository extends ServiceEntityRepository RelatedRepository.php:11
RelatedRepository::__construct()
public function __construct(ManagerRegistry $registry)RelatedRepository::findFor()
public function findFor(Video $video, int $limit = 12): array- Returns
a film's neighbours, the closest first, still listed
RelatedRepository::scoresFor()
public function scoresFor(array $videoIds, int $limit = 100): array$videoIds@return array<int, float> neighbour id => summed score over $videoIds
RelatedRepository::deleteFor()
public function deleteFor(Video $video): voidRelatedRepository::deleteAll()
public function deleteAll(): voidSourceRepository class
class SourceRepository extends ServiceEntityRepository SourceRepository.php:10
SourceRepository::__construct()
public function __construct(ManagerRegistry $registry)SubscriptionRepository class
class SubscriptionRepository extends ServiceEntityRepository SubscriptionRepository.php:12
SubscriptionRepository::__construct()
public function __construct(ManagerRegistry $registry)SubscriptionRepository::findOneFor()
public function findOneFor(User $user, Channel $channel): ?SubscriptionSubscriptionRepository::countFor()
public function countFor(Channel $channel): intSubscriptionRepository::findChannels()
public function findChannels(User $user): array- Returns
the channels a member follows
SubscriptionRepository::findSubscribers()
public function findSubscribers(Channel $channel, int $limit = 100): array- Returns
a channel's subscribers, the newest first
VideoCommentRepository class
class VideoCommentRepository extends ServiceEntityRepository VideoCommentRepository.php:12
VideoCommentRepository::__construct()
public function __construct(ManagerRegistry $registry)VideoCommentRepository::findVisible()
public function findVisible(Video $video, int $limit = 100, int $offset = 0): array- Returns
the comments online under a film (not the replies), the pinned one first, then the newest
VideoCommentRepository::countVisible()
public function countVisible(Video $video): intVideoCommentRepository::findForVideo()
public function findForVideo(Video $video): array- Returns
every comment of a film, for its author's studio
VideoCommentRepository::countPending()
public function countPending(): intVideoCommentRepository::unpinAll()
public function unpinAll(Video $video): voidVideoRepository class
class VideoRepository extends ServiceEntityRepository VideoRepository.php:15
VideoRepository::__construct()
public function __construct(ManagerRegistry $registry, string $class = Video::class)VideoRepository::listed()
public function listed(string $alias = 'v'): QueryBuilderListed: public, published, not taken down, playable.
VideoRepository::findLatest()
public function findLatest(int $limit = 24, int $offset = 0, ?Channel $channel = null): array- Returns
the newest first
VideoRepository::findPopular()
public function findPopular(int $limit = 24, ?Channel $channel = null): array- Returns
the most seen of all time (ours and the imported views)
VideoRepository::findTrending()
public function findTrending(int $limit = 24, int $days = 7): array- Returns
the most watched of the last $days days (counted sittings), the most seen of all time to fill up
VideoRepository::findOneReachable()
public function findOneReachable(string $slug, ?User $viewer = null, bool $staff = false): ?VideoThe video at that address, if its visitor may see it (listed, unlisted, or theirs).
VideoRepository::findOneByUploadId()
public function findOneByUploadId(string $uploadId): ?VideoVideoRepository::findForStudio()
public function findForStudio(User $user, ?string $visibility = null): array- Returns
a member's own, in the studio (every state but the trash)
VideoRepository::sumUploads()
public function sumUploads(User $user): intBytes of originals a member has sent: what the quota counts.
VideoRepository::findFromSubscriptions()
public function findFromSubscriptions(User $user, int $limit = 48): array- Returns
the new videos of the channels a member follows
VideoRepository::searchListed()
public function searchListed(string $query, array $filters = [], int $limit = 24, int $offset = 0): arrayThe database's search, when Typesense is not there: every word in the title, the description, the channel's name.
VideoRepository::filter()
public static function filter(QueryBuilder $qb, array $filters): QueryBuilderVideoRepository::durationBounds()
public static function durationBounds(?string $duration): arrayshort: under 4 minutes; medium: 4 to 20; long: over 20.
@return array{0: ?int, 1: ?int}
VideoRepository::dateSince()
public static function dateSince(?string $date): ?\DateTimeVideoRepository::countByProcessing()
public function countByProcessing(): array- Returns
processing state => videos, for the back office
VideoRepository::findProcessing()
public function findProcessing(int $limit = 10): arrayVideoRepository::findAllListed()
public function findAllListed(int $limit = 5000): array- Returns
listed, by id: what the suggestions compute over
VideoRepository::findListedByIds()
public function findListedByIds(array $ids): array$ids@return list<Video> listed, in the order of $ids
WatchEventRepository class
class WatchEventRepository extends ServiceEntityRepository WatchEventRepository.php:12
WatchEventRepository::__construct()
public function __construct(ManagerRegistry $registry)WatchEventRepository::findOneByToken()
public function findOneByToken(Video $video, string $token): ?WatchEventWatchEventRepository::findHistory()
public function findHistory(User $user, int $limit = 60): arrayA member's history: each video once, the last watched first.
WatchEventRepository::findWatchedIds()
public function findWatchedIds(?User $user, ?string $visitor = null, int $limit = 200): array- Returns
the videos a member (or a visitor) watched, the last first
WatchEventRepository::findResume()
public function findResume(User $user, Video $video): ?floatWhere a member stopped last time, in seconds (null: never, or near the end).
WatchEventRepository::clearHistory()
public function clearHistory(User $user): intWatchEventRepository::removeFromHistory()
public function removeFromHistory(User $user, Video $video): intWatchEventRepository::countUnflushed()
public function countUnflushed(int $limit = 5000): array- Returns
video id => views counted and not yet added to Video::$views
WatchEventRepository::markFlushed()
public function markFlushed(array $videoIds): intWatchEventRepository::findCoWatching()
public function findCoWatching(int $days = 90, int $limit = 200000): arrayWho watched what, for the suggestions' co-watching: viewer key (the account, else the visitor) => the videos they watched enough to count.
WatchEventRepository::stats()
public function stats(Video $video, int $days = 30, int $buckets = 20): arrayThe studio's figures for one film.
WatchEventRepository::figures()
public static function figures(array $rows, int $duration, int $days = 30, int $buckets = 20, ?\DateTimeInterface $today = null): arrayThe figures from raw sittings - separate, so they are tested without a database. The retention curve: for each twentieth of the film, the share of the sittings that reached it.
WatchEventRepository::countViewsSince()
public function countViewsSince(int $days = 30): intCounted views over the last $days days, all videos: the dashboard's figure.
Base\Video\Search
SafeIndexer class
class SafeIndexer extends TypesenseIndexer SafeIndexer.php:18
glitchr/typesense-bundle's indexer, made safe for a search server that is down: whatever the client throws (the bundle only catches its own and php-http's exceptions - a PSR-18 network error from symfony/http-client went through and failed the page that saved a film), the index misses a change, the save does not fail and no visitor is told about Typesense.
bin/console typesense:action upsert fills the index again. It also
forgets what it has sent, so a long-running worker does not send it twice.
SafeIndexer::setLogger()
public function setLogger(?LoggerInterface $logger): voidSafeIndexer::postFlush()
public function postFlush(PostFlushEventArgs $args)VideoSearch class
class VideoSearch VideoSearch.php:18
The platform's search: Typesense (glitchr/typesense-bundle, the "video" mapping of the site's config/packages/typesense.yaml - docs/search.md) with typo tolerance and prefixes, filtered by length, date, category and channel, the listed films only. With no such mapping, or the server down, the database answers (every word in the title, the text, the channel).
const PER_PAGE = 24
VideoSearch::__construct()
public function __construct(readonly VideoRepository $videos, readonly ?TypesenseFinderInterface $finder = null)VideoSearch::search()
public function search(string $term, array $filters = [], int $page = 1): arrayVideoSearch::typesenseFilters()
public static function typesenseFilters(array $filters): array- Returns
Typesense's filter_by clauses for the filters
Base\Video\Security
VideoVoter class
final class VideoVoter extends Voter VideoVoter.php:20
VIDEO_VIEW a film reachable (listed or unlisted), or its owners', or the moderators' VIDEO_EDIT its owners (and its channel's), or an administrator VIDEO_MODERATE the moderators (ROLE_EDITOR and up): take down, hide a comment Also on a Channel (VIDEO_EDIT: its owners) and a Playlist (VIDEO_VIEW, VIDEO_EDIT).
const VIEW = 'VIDEO_VIEW'const EDIT = 'VIDEO_EDIT'const MODERATE = 'VIDEO_MODERATE'
VideoVoter::__construct()
public function __construct(readonly AccessDecisionManagerInterface $decisions)Base\Video\Service
Channels class
class Channels Channels.php:17
A member's channel, made the first time they publish (named after them, renamed in the studio), and the subscriptions to channels, whose count the channel keeps.
Channels::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly ChannelRepository $channels, readonly SubscriptionRepository $subscriptions)Channels::forMember()
public function forMember(User $user, bool $create = true): ?ChannelChannels::isSubscribed()
public function isSubscribed(?User $user, Channel $channel): boolChannels::toggle()
public function toggle(User $user, Channel $channel): boolFollow, or stop following: whether $user follows $channel now. A member never follows their own.
Comments class
class Comments Comments.php:20
A comment left under a film: omnibase's comment (its form, its guard, Akismet's score) made a VideoComment with the moment of the film it was written at, a reply attached to its comment (one level), the video's authors told. A video whose comments are closed takes none.
Comments::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly VideoCommentRepository $comments, readonly Notifier $notifier, readonly bool $autoApprove = true)Comments::create()
public function create(Video $video, CommentModel $model, ?User $user, Request $request, ?int $moment = null, ?int $parentId = null): VideoCommentComments::pin()
public function pin(VideoComment $comment, bool $pinned = true): voidPinned first under the film (one at a time), or unpinned.
Comments::hide()
public function hide(VideoComment $comment, bool $hidden = true): voidHidden by the film's author (omnibase's trash state), or shown again.
Editor class
class Editor Editor.php:20
What the studio writes: a film's details applied (the keywords made omnibase tags, the poster a still or a picture sent), a film of another platform added by its address.
Editor::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly Pictures $pictures, readonly Channels $channels, readonly Embeds $embeds, readonly Storage $storage)Editor::apply()
public function apply(VideoDetails $details, Video $video): voidEditor::link()
public function link(LinkDetails $details, User $user): ?VideoA film of another platform: its page, framed through embed_url(); null when nothing can frame it.
Editor::tags()
public function tags(array $keywords): arrayThe tags for these keywords, found by their slug or made.
Moderation class
class Moderation Moderation.php:20
Reports and take-downs. A report is omnibase's Complaint (source "video"), handled on omnibase/admin's screen for them: what is reported (a video, a comment, a channel) is its location, its author the target.
A take-down hides the film with a reason its authors are told (the DSA's "statement of reasons"); restoring it puts it back private.
const REASONS = ['illegal', 'violence', 'hate', 'sexual', 'harassment', 'spam', 'copyright', 'privacy', 'other']
Moderation::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly UrlGeneratorInterface $urls, readonly Notifier $notifier)Moderation::report()
public function report(Video|VideoComment|Channel $subject, ?User $reporter, string $reason, string $text = ''): ComplaintModeration::takeDown()
public function takeDown(Video $video, string $reason): voidModeration::restore()
public function restore(Video $video): voidModeration::subjectOf()
public function subjectOf(Complaint $complaint): Video|VideoComment|Channel|nullThe video or comment a complaint of ours is about.
Notifier class
class Notifier Notifier.php:20
What a video's authors are told, in the bell of omnibase's notification centre ('notify', in-app): their film is ready, or failed; somebody commented it; the moderators took it down, and why. A notification that cannot be sent (no notifier in a test, a mailer down) never stops what caused it.
Notifier::__construct()
public function __construct(readonly TranslatorInterface $translator, readonly UrlGeneratorInterface $urls, readonly ?LoggerInterface $logger = null)Notifier::transcoded()
public function transcoded(Video $video): voidNotifier::failed()
public function failed(Video $video): voidNotifier::commented()
public function commented(VideoComment $comment): voidNotifier::takenDown()
public function takenDown(Video $video): voidPictures class
class Pictures Pictures.php:16
The pictures a member sends in the studio - a poster of their own, a channel's picture and banner - kept with the films (served by the same address), under random names.
const TYPES = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']
Pictures::__construct()
public function __construct(readonly Storage $storage, readonly string $publicPath = '/media/videos')Pictures::poster()
public function poster(Video $video, UploadedFile $file): stringPictures::channel()
public function channel(Channel $channel, UploadedFile $file, string $kind): stringProcessing class
class Processing Processing.php:19
A film's way from the uploaded file to what plays: running, the ladder built beside the old one (a replacement keeps playing until the new one is whole), the sources, the poster unless its author chose one, the storyboard, ready - or failed, with why. The original is removed once the ladder exists (video.upload.keep_originals: 0).
Processing::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly Storage $storage, readonly Transcoder $transcoder, readonly Notifier $notifier, readonly string $uploads, readonly bool $autoPublish = false, readonly int $keepOriginals = 0, readonly ?LoggerInterface $logger = null)Processing::transcode()
public function transcode(Video $video, string $source): boolProcessing::frames()
public function frames(Video $video): array- Returns
the stills a poster can be chosen from
Processing::removeUpload()
public function removeUpload(string $source): voidtusd's file and its .info, if they are where uploads are kept.
Ratings class
class Ratings Ratings.php:18
"J'aime" and "je n'aime pas", on omnibase's own Like (Base\Entity\Thread\Like, on any thread): a thumbs-up is the core's icon, a thumbs-down another icon on the same row. One rating a member and film; choosing the other one turns it over, choosing the same one again takes it back. The thread's row is locked while it is decided (a double click made two rows).
const UP = 'fa-solid fa-thumbs-up'const DOWN = 'fa-solid fa-thumbs-down'
Ratings::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly Views $views)Ratings::of()
public function of(?User $user, Video $video): ?string- Returns
what $user says of $video
Ratings::count()
public function count(Video $video): arrayRatings::rate()
public function rate(User $user, Video $video, string $rating): ?string- Returns
the member's rating after it
RemoteManifest class
class RemoteManifest RemoteManifest.php:21
Someone else's HLS, played without a copy. A master playlist that its origin serves without CORS is read here, kept a while (video.remote.ttl) and given to the browser from our address, its URIs made absolute: the variant playlists and the segments are then fetched by the browser from the origin itself, which allows it. Nothing else of the film passes through the site.
Only the hosts in video.remote.hosts are read (no open proxy).
RemoteManifest::__construct()
public function __construct(readonly HttpClientInterface $client, readonly CacheInterface $cache, readonly array $hosts = [], readonly int $ttl = 3600)RemoteManifest::allows()
public function allows(string $url): boolRemoteManifest::master()
public function master(string $url): stringThe master playlist at $url, its URIs absolute.
RemoteManifest::duration()
public function duration(string $url): ?intThe film's length: the first variant's #EXTINF summed (null when unreadable).
RemoteManifest::absolutize()
public static function absolutize(string $playlist, string $base): stringEvery URI line and URI="..." attribute made absolute against $base.
RemoteManifest::variants()
public static function variants(string $master): array- Returns
the variant playlists of a master, in its order
RemoteManifest::sum()
public static function sum(string $playlist): intSeconds, the #EXTINF of a media playlist summed.
RemoteManifest::resolve()
public static function resolve(string $uri, string $base): stringStorage class
class Storage Storage.php:16
Where a video's files live: one directory a video under video.storage, named by a random key (a private film's files cannot be guessed), served by the web server at video.public_path. What the entities keep are the public addresses (/media/videos/<key>/master.m3u8), so the pages need no service to show a poster.
Storage::__construct()
public function __construct(readonly string $root, readonly string $publicPath = '/media/videos')Storage::getRoot()
public function getRoot(): stringStorage::directory()
public function directory(Video $video, bool $create = true): stringThe video's directory, its key made the first time.
Storage::workspace()
public function workspace(Video $video): stringA fresh directory beside it, to build a new ladder before it replaces the old one.
Storage::promote()
public function promote(Video $video, string $workspace): voidThe workspace becomes the video's directory; the previous files go.
Storage::discard()
public function discard(string $workspace): voidStorage::url()
public function url(Video $video, string $file): stringThe address a file of the video's directory is served at.
Storage::pathOf()
public function pathOf(string $url): ?stringThe file behind one of our addresses, or null for anything else.
Storage::remove()
public function remove(Video $video): voidStorage::size()
public function size(Video $video): intBytes a video's files take.
Transcoder class
class Transcoder Transcoder.php:28
A film turned into what the player reads, with ffmpeg (symfony/process):
master.m3u8 the HLS master, one variant a rung <height>/index.m3u8 each rung: fMP4 segments (seg_000.m4s...) and its init poster.jpg the still (frame-2.jpg: about a third in) frame-<n>.jpg stills the author picks a poster from storyboard.jpg/.vtt the time bar's previews: a sprite and its boxes
The ladder is 1080, 720, 480, 360 by default (video.transcode.renditions), never above the source: a 720p film gets 720, 480, 360. Every rung has a key frame on each segment's start (-force_key_frames), so the player can switch between them at any boundary.
The *Command() methods only assemble arguments - no file is touched - so they are read and tested as they are. Modelled on omnibase/social's Renderer.
const BITRATES = [2160 => [14000, 16000], 1440 => [8000, 9000], 1080 => [5000, 5600], 720 => [2800, 3100], 480 => [1400, 1600], 360 => [800, 900], 240 => [400, 450]]Height => [video bit rate, its ceiling], kb/s.
const THUMB_WIDTH = 160const SPRITE_COLUMNS = 10
Transcoder::__construct()
public function __construct(readonly string $ffmpeg = 'ffmpeg', readonly string $ffprobe = 'ffprobe', readonly array $renditions = [1080, 720, 480, 360], readonly int $segment = 6, readonly string $preset = 'veryfast', readonly int $timeout = 14400, readonly int $thumbnails = 100, readonly int $frames = 4)Transcoder::transcode()
public function transcode(string $source, string $directory): arrayEverything for one film, written into $directory.
Transcoder::ladder()
public function ladder(int $width, int $height): arrayThe rungs for a source of that size: the configured heights not above it (its own height when it is below them all), the width kept to the source's shape and even.
Transcoder::hlsCommand()
public function hlsCommand(string $source, string $directory, array $rungs, bool $audio = true): arrayffmpeg's arguments for the whole ladder in one pass: the picture split and scaled per rung, the sound once a rung, fMP4 HLS with a master.
Transcoder::stillCommand()
public function stillCommand(string $source, float $second, string $output): array- Returns
one frame at $second, 1280 wide at most
Transcoder::storyboard()
public function storyboard(float $duration, int $width, int $height): ?arrayThe sprite's shape: one picture every interval seconds, at most
video.transcode.thumbnails of them, ten a row.
Transcoder::storyboardCommand()
public function storyboardCommand(string $source, string $output, array $board): arrayTranscoder::storyboardVtt()
public static function storyboardVtt(float $duration, array $board, string $sprite): stringThe WebVTT the time bar reads its previews from: a cue a picture, pointing at its box in the sprite (#xywh=x,y,w,h).
Transcoder::frameTimes()
public function frameTimes(float $duration): array- Returns
where the stills offered as a poster are taken
Transcoder::probe()
public function probe(string $path): arrayWhat ffprobe measures: length, picture size (turned when the film is), sound, weight.
Transcoder::measures()
public static function measures(array $json, string $path = ''): arrayTranscoder::bitrate()
public static function bitrate(int $height): arrayTranscoder::even()
public static function even(int|float $value): intThe nearest even number (853.3 -> 854): x264 wants even sides.
Transcoder::vttTime()
public static function vttTime(float $seconds): string00:01:02.500
Uploads class
class Uploads Uploads.php:29
The resumable uploads, with tusd. The studio hands the browser a signed token (who, until when, and - for a replacement - which video); Uppy sends it in the upload's metadata; tusd asks the site twice:
pre-create may this upload start? - a valid token, a member allowed to send (verified, not banned), a file under the size limit, the quota and the daily limiter respected post-finish the file is whole - the video is made (or the replaced one found), a draft, and its transcoding queued
tusd's HTTP hooks (v2): a JSON request {Type, Event: {Upload: {ID, Size, MetaData, Storage: {Path}}, HTTPRequest}}, answered with } to go on or {RejectUpload: true, HTTPResponse: {StatusCode, Body}} to refuse.
Uploads::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly VideoRepository $videos, readonly Channels $channels, readonly MessageBusInterface $bus, readonly RateLimiterFactoryInterface $limiter, readonly string $secret, readonly bool $enabled = true, readonly int $quota = 0, readonly int $maxSize = 0, readonly bool $verifiedOnly = true, readonly string $directory = '', readonly int $ttl = 86400)Uploads::isEnabled()
public function isEnabled(): boolUploads::token()
public function token(User $user, ?Video $replace = null, ?int $now = null): stringA token for $user's next upload (or the replacement of $replace's file).
Uploads::verify()
public function verify(?string $token, ?int $now = null): ?array- Returns
what a valid token says
Uploads::remaining()
public function remaining(User $user): ?intBytes $user may still send (null: no quota).
Uploads::refusal()
public function refusal(?User $user, int $size, bool $consume = false): ?stringWhy $user may not send a file of $size bytes now, or null.
Uploads::hook()
public function hook(array $hook): arraytusd's question, answered.
- Returns
the hook's response
Uploads::finish()
public function finish(User $user, string $uploadId, string $path, int $size, string $filename, ?int $replace = null): ?VideoThe video made of a finished upload (or the one whose file it replaces), its transcoding queued.
Uploads::slug()
public static function slug(): stringA short random address part, already what omnibase's Slugify keeps (lowercase): /v/3f9a0c71be42.
Uploads::isUploadPath()
public function isUploadPath(string $path): boolInside the uploads directory, as tusd wrote it (never a path a forged hook names elsewhere).
Views class
class Views Views.php:26
The player's beacons, and the rule that makes a sitting a view: once min(30 s, half the film) has really been played (video.views.min_seconds, .ratio), and only once per visitor and film every six hours (video.views.dedupe) - the visitor being the account, or a hash of the address and the browser that never leaves this service. Counted views are added to Video::$views by flush() (video:views:flush, every few minutes from cron), not one UPDATE per beacon.
Views::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly WatchEventRepository $events, readonly CacheInterface $cache, readonly MessageBusInterface $bus, readonly string $secret, readonly int $minSeconds = 30, readonly float $ratio = 0.5, readonly int $dedupe = 21600, readonly string $provider = 'local', readonly string $gorseUrl = '')Views::threshold()
public function threshold(?int $duration): floatSeconds of playing that make a view of this film.
Views::visitor()
public function visitor(Request $request, ?User $user = null): stringViews::record()
public function record(Video $video, array $beacon, string $visitor, ?User $user = null, ?\DateTimeInterface $now = null): ?WatchEventA beacon: {token, event: start|progress|end, position, watched, source, referrer}.
Returns the sitting, or null for a beacon that cannot be one.
Views::flush()
public function flush(): intThe views counted since the last flush, added to each Video::$views. Returns how many.
Views::feedback()
public function feedback(string $type, ?User $user, Video $video): voidBase\Video\Suggest
AffinityInterface interface
interface AffinityInterface AffinityInterface.php:14
What else makes two films close, from another bundle: omnibase/broadcast says "same artist" (as strong as the same channel) and "same episode" (as strong as the same list). The calculator adds, for a pair, the weight of each strength they share once: same channel and same artist is 3, not 6.
AffinityInterface::groups()
public function groups(array $videos): array- Returns
video id => group key ("artist:12") => weight (3.0)
GorseSuggest class
class GorseSuggest implements SuggestInterface GorseSuggest.php:25
Gorse (gorse-in-one, Apache-2.0), spoken to over its REST API with symfony/http-client - no client library:
POST /api/feedback read, watch, like (sent by Messenger) GET /api/recommend/{user}?n= for a member POST /api/session/recommend?n= for a visitor, from what they watched GET /api/item/{id}/neighbors?n= "à suivre"
The items are the videos' ids, the users the accounts' ids (Gorse adds them on their first feedback). Any failure throws: Suggestions catches it and answers with the local suggestions.
GorseSuggest::__construct()
public function __construct(readonly HttpClientInterface $client, readonly VideoRepository $videos, readonly string $url = '', readonly string $key = '', readonly ?SettingBagInterface $settings = null)GorseSuggest::isEnabled()
public function isEnabled(): boolGorseSuggest::feedback()
public function feedback(string $type, User $user, Video $video): voidGorseSuggest::send()
public function send(string $type, string $userId, int $videoId, ?\DateTimeInterface $at = null): voidGorseSuggest::recommend()
public function recommend(User $user, int $limit = 24): arrayGorseSuggest::session()
public function session(array $videoIds, int $limit = 24): arrayGorseSuggest::similar()
public function similar(Video $video, int $limit = 12): arrayGorseSuggest::ids()
public static function ids(array $scores): array[{Id, Score}...] or ["12", "7"...] => [12, 7]
LocalSuggest class
class LocalSuggest implements SuggestInterface LocalSuggest.php:20
The suggestions computed here: a film's neighbours from the Related table (RelatedCalculator, at night); for a member or a visitor, the neighbours of what they watched last, summed, what they already saw left out, and one in five taken from the trends so the feed is not only more of the same. No more than video.suggest.per_channel films of one channel in a list.
LocalSuggest::__construct()
public function __construct(readonly RelatedRepository $related, readonly VideoRepository $videos, readonly WatchEventRepository $events, readonly int $perChannel = 2)LocalSuggest::feedback()
public function feedback(string $type, User $user, Video $video): voidLocalSuggest::recommend()
public function recommend(User $user, int $limit = 24): arrayLocalSuggest::session()
public function session(array $videoIds, int $limit = 24): arrayLocalSuggest::similar()
public function similar(Video $video, int $limit = 12): arrayLocalSuggest::cap()
public function cap(array $videos, int $limit, ?int $per = null): array- Returns
at most $per (per_channel by default) of one channel, then $limit in all
RelatedCalculator class
class RelatedCalculator RelatedCalculator.php:31
The Related table, computed again at night (video:related). For two listed films A and B, B's score after A:
3 · same channel, or same artist (an AffinityInterface's 3.0 groups) + 2 · same public list, or same episode (its 2.0 groups) + 1 · each keyword in common (three at most) + 2.5 · co-watching (the cosine of their audiences over 90 days) + 0.5 · B's freshness (e^(-age/30 days)) + 0.5 · B's popularity (log of its views against the most seen)
A strength shared twice counts once (same channel and same artist: 3). Only pairs that share something are scored - a group shared by more than MAX_GROUP films says nothing and only scores pairs found otherwise. Each film keeps its NEIGHBOURS best, no more than video.suggest.per_channel from one channel.
RelatedCalculator::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly VideoRepository $videos, readonly PlaylistRepository $playlists, readonly WatchEventRepository $events, readonly iterable $affinities = [], readonly int $perChannel = 2)RelatedCalculator::compute()
public function compute(?\DateTimeInterface $now = null): int- Returns
the rows written
RelatedCalculator::score()
public function score(array $videos, array $groups, array $audiences, \DateTimeInterface $now): arrayThe scoring itself, on plain data - tested without a database.
$groupsvideo id => group => weight
$audiencesvideo id => viewer => true
- Returns
[video id, neighbour id, score]
RelatedCalculator::pair()
public function pair(Video $a, Video $b, array $groupsA, array $groupsB, array $keywordsA, array $keywordsB, array $audienceA, array $audienceB, int $maxViews, \DateTimeInterface $now): floatRelatedCalculator::groups()
public function groups(array $videos): arrayThe groups of each film: its channel (3), its public lists (2), and whatever the affinities add.
SuggestInterface interface
interface SuggestInterface SuggestInterface.php:14
What to watch next. Two implementations: LocalSuggest (always there: the Related table computed at night, the member's history, the trends) and GorseSuggest (gorse-in-one, optional). Suggestions picks the configured one and falls back to the local one: Gorse off or down, nothing breaks.
SuggestInterface::feedback()
public function feedback(string $type, User $user, Video $video): voidA gesture: read (playback started), watch (a view counted), like, or "not interested" (negative).
SuggestInterface::recommend()
public function recommend(User $user, int $limit = 24): array- Returns
for a member, from what they watched and liked
SuggestInterface::session()
public function session(array $videoIds, int $limit = 24): array$videoIdswhat a visitor watched, the last first @return list<Video>
SuggestInterface::similar()
public function similar(Video $video, int $limit = 12): array- Returns
"à suivre" after this one
Suggestions class
class Suggestions implements SuggestInterface Suggestions.php:16
The suggestions the pages ask for (SuggestInterface is this): Gorse when video.suggest.provider is "gorse" and it answers enough, the local ones otherwise - and to fill a short answer. A visitor's own films never come back to them.
Suggestions::__construct()
public function __construct(readonly LocalSuggest $local, readonly GorseSuggest $gorse, readonly string $provider = 'local', readonly ?LoggerInterface $logger = null)Suggestions::usesGorse()
public function usesGorse(): boolSuggestions::feedback()
public function feedback(string $type, User $user, Video $video): voidSuggestions::recommend()
public function recommend(User $user, int $limit = 24): arraySuggestions::session()
public function session(array $videoIds, int $limit = 24): arraySuggestions::similar()
public function similar(Video $video, int $limit = 12): arrayBase\Video\Twig
VideoExtension class
class VideoExtension extends AbstractExtension VideoExtension.php:32
The pages' words for films:
seconds|video_time 1:02:03 text|timecodes(video) the text escaped, its "1:23" made links that seek the player n|video_count 1,2 k / 3,4 M (the locale's comma) date|video_ago "il y a 3 jours" text|video_excerpt(160) the text without its tags, cut at a word video_rating(video) {up, down, mine} video_subscribed(channel) whether the member follows it video_upload_token(video?) the signed token the studio's uploader sends video_categories() the categories configured
VideoExtension::__construct()
public function __construct(readonly TranslatorInterface $translator, readonly UrlGeneratorInterface $urls, readonly Ratings $ratings, readonly Channels $channels, readonly Uploads $uploads, readonly \Symfony\Bundle\SecurityBundle\Security $security, readonly array $categories = [], readonly array $licenses = [])VideoExtension::getFilters()
public function getFilters(): arrayVideoExtension::getFunctions()
public function getFunctions(): arrayVideoExtension::timecodes()
public function timecodes(?string $text, ?Video $video = null): stringThe text escaped, line breaks kept, addresses linked, and every moment (1:23, 1:02:03) a link to the film at that second - the player seeks there instead of loading the page again.
VideoExtension::excerpt()
public static function excerpt(?string $text, int $length = 160): stringThe text without its tags, on one line, cut at a word under $length characters.
VideoExtension::count()
public function count(int|float|null $n, ?string $locale = null): stringVideoExtension::ago()
public function ago(?\DateTimeInterface $date, ?\DateTimeInterface $now = null): stringVideoExtension::rating()
public function rating(Video $video): arrayVideoExtension::subscribed()
public function subscribed(Channel $channel): boolVideoExtension::uploadToken()
public function uploadToken(?Video $replace = null): ?string