Aller au contenu

omnibase/video Référence de l’API

Étage
Structure préparée · Omnibase
Version
main
Vérifié en réel
Son README ne le dit pas
Licence
MIT
composer require omnibase/video:dev-main

Lue dans les fichiers PHP du paquet au commit e560817678d2 : ses espaces de noms, classes, interfaces et énumérations, leurs constantes, propriétés et méthodes publiques avec leur PHPDoc, et les options que prennent ses fabriques. Rien n’en est exécuté.

Base\Video

  • VideoBundle class

    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.

    3 methods

Base\Video\Admin\Settings

  • GorseKeySection class

    Gorse's server key (video.suggest.provider: gorse), on the API keys page: typed there it wins over the configuration.

    3 methods

Base\Video\Admin\Widget

  • VideoOverviewWidgetType class

    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.

    4 methods

Base\Video\Command

  • CleanUploadsCommand class

    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.

    2 methods

  • FlushViewsCommand class

    The views counted since the last run, added to Video::$views - from cron, every few minutes.

    2 methods

  • RelatedCommand class

    The suggestions' Related table computed again - from cron, at night.

    2 methods

  • TranscodeCommand class

    One film transcoded here and now, from a file: a back-office repair, an import.

    2 methods

Base\Video\Controller\Admin

  • OpenToEditorsTrait trait

    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

    The channels: their names and addresses, their owners, how many follow them.

    4 methods

  • VideoCommentCrudController class

    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.

    8 methods

  • VideoCrudController class

    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.

    8 methods

Base\Video\Controller\Client

  • ChannelController class

    A channel's page - its shelves, its videos, its lists, what it says - and following it.

    3 methods · 1 constant

  • LibraryController class

    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.

    10 methods

  • SearchController class

    The search: one bar, the platform's films (Typesense, the database when it is down), its filters.

    2 methods

  • StudioController class

    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.

    14 methods

  • UploadController class

    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.

    1 method

  • WatchController class

    A 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

Base\Video\Demo

  • VideoDemoAccounts class

    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).

    2 methods

Base\Video\DependencyInjection

  • VideoConfiguration class

    config/packages/video.yaml. Every leaf is also a flat parameter (video.upload.enabled, video.suggest.provider...): docs/configuration.md.

    1 method

  • VideoExtension class

    3 methods

Base\Video\Entity

  • Channel class

    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).

    26 methods · 1 constant

  • Playlist class

    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.

    27 methods · 6 constants

  • PlaylistItem class

    7 methods

  • Related class

    "À suivre": a video's neighbours and how close they are, computed at night by Suggest\RelatedCalculator (video:related). Read by LocalSuggest.

    6 methods

  • Source class

    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:

    23 methods · 7 constants

  • Subscription class

    A member following a channel: its new videos in their feed (/abonnements).

    5 methods

  • Video class

    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.

    78 methods · 10 constants

  • VideoComment class

    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.

    8 methods · 1 constant

  • WatchEvent class

    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.

    19 methods · 1 constant

Base\Video\Exception

  • RemoteException class

    A remote playlist that cannot be read, or is not allowed (video.remote.hosts).

  • TranscodingException class

    ffmpeg or ffprobe missing, failing, or a film it cannot read.

    1 method

Base\Video\Form

Base\Video\Message

  • ComputeRelated class

    The suggestions' Related table computed again (video:related --async, at night).

  • FlushViews class

    The views counted since the last flush, added to Video::$views (video:views:flush --async, from cron).

  • SuggestFeedback class

    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.

    1 method · 4 properties

  • TranscodeVideo class

    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).

    1 method · 2 properties

Base\Video\MessageHandler

Base\Video\Model

  • ChannelDetails class

    1 method · 6 properties

  • LinkDetails class

    A film of another platform, added by its address (framed through embed_url()).

    2 properties

  • PlaylistDetails class

    1 method · 3 properties

  • VideoDetails class

    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.

    2 methods · 11 properties

Base\Video\Repository

Base\Video\Search

  • SafeIndexer class

    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.

    2 methods

  • VideoSearch class

    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).

    3 methods · 1 constant

Base\Video\Security

  • VideoVoter class

    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).

    1 method · 3 constants

Base\Video\Service

  • Channels class

    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.

    4 methods

  • Comments class

    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.

    4 methods

  • Editor class

    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.

    4 methods

  • Moderation class

    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.

    5 methods · 1 constant

  • Notifier class

    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.

    5 methods

  • Pictures class

    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.

    3 methods · 1 constant

  • Processing class

    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).

    4 methods

  • Ratings class

    "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

  • RemoteManifest class

    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.

    8 methods

  • Storage class

    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.

    10 methods

  • Transcoder class

    A film turned into what the player reads, with ffmpeg (symfony/process):

    14 methods · 3 constants

  • Uploads class

    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:

    10 methods

  • Views class

    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.

    6 methods

Base\Video\Suggest

  • AffinityInterface interface

    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.

    1 method

  • GorseSuggest class

    Gorse (gorse-in-one, Apache-2.0), spoken to over its REST API with symfony/http-client - no client library:

    8 methods

  • LocalSuggest class

    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.

    6 methods

  • RelatedCalculator class

    The Related table, computed again at night (video:related). For two listed films A and B, B's score after A:

    5 methods · 2 constants

  • SuggestInterface interface

    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.

    4 methods

  • Suggestions class

    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.

    6 methods

Base\Video\Twig

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(): string

Modern layout: the class lives in src/, the bundle root is the package root.

VideoBundle::build()

public function build(ContainerBuilder $container): void

Base\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(): string

GorseKeySection::getFields()

public function getFields(): array

Base\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(): string

VideoOverviewWidgetType::getTemplate()

public function getTemplate(): string

VideoOverviewWidgetType::getTemplateVars()

public function getTemplateVars(MenuItem $widget): array

Base\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): int

FlushViewsCommand 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): int

RelatedCommand 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): int

TranscodeCommand 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): int

Base\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(): string

ChannelCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

ChannelCrudController::configureFields()

public function configureFields(string $pageName): iterable

ChannelCrudController::configureActions()

public function configureActions(Actions $actions): Actions

VideoCommentCrudController 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(): string

VideoCommentCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

VideoCommentCrudController::configureFilters()

public function configureFilters(Filters $filters): Filters

VideoCommentCrudController::configureFields()

public function configureFields(string $pageName): iterable

VideoCommentCrudController::configureActions()

public function configureActions(Actions $actions): Actions

VideoCommentCrudController::approve()

public function approve(string $entityId): Response

VideoCommentCrudController::spam()

public function spam(string $entityId): Response

VideoCommentCrudController::trash()

public function trash(string $entityId): Response

VideoCrudController 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): void

VideoCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

VideoCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

VideoCrudController::configureFilters()

public function configureFilters(Filters $filters): Filters

VideoCrudController::configureFields()

public function configureFields(string $pageName): iterable

VideoCrudController::configureActions()

public function configureActions(Actions $actions): Actions

VideoCrudController::takedown()

public function takedown(Request $request, string $entityId): Response

A reason first (the DSA's statement of reasons, sent to the authors), then down.

VideoCrudController::restore()

public function restore(string $entityId): Response

Base\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): Response

ChannelController::subscribe()

public function subscribe(Request $request, string $slug): Response

LibraryController 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): Response

LibraryController::clearHistory()

public function clearHistory(Request $request, WatchEventRepository $events): Response

LibraryController::later()

public function later(): Response

LibraryController::subscriptions()

public function subscriptions(SubscriptionRepository $subscriptions): Response

LibraryController::playlists()

public function playlists(Request $request): Response

LibraryController::playlist()

public function playlist(Request $request, string $slug): Response

LibraryController::toggle()

public function toggle(Request $request): Response

Add 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): Response

LibraryController::order()

public function order(Request $request, string $slug): Response

SearchController 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 = [])
public function search(Request $request, ChannelRepository $channels): Response

StudioController 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): Response

StudioController::upload()

public function upload(): Response

StudioController::uploaded()

public function uploaded(string $uploadId): Response

Where the uploader goes once a file is whole: the film's page in the studio, as soon as the hook made it.

public function link(Request $request, Editor $editor): Response

StudioController::edit()

public function edit(Request $request, int $id, Editor $editor, Processing $processing): Response

StudioController::status()

public function status(int $id, TranslatorInterface $translator): JsonResponse

StudioController::stats()

public function stats(int $id, WatchEventRepository $events): Response

StudioController::comments()

public function comments(Request $request, int $id, VideoCommentRepository $comments, Comments $service): Response

StudioController::replace()

public function replace(int $id): Response

StudioController::delete()

public function delete(Request $request, int $id): Response

To 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): Response

StudioController::channel()

public function channel(Request $request, ChannelRepository $channels, Pictures $pictures): Response

StudioController::subscribers()

public function subscribers(SubscriptionRepository $subscriptions): Response

UploadController 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): Response

WatchController 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): Response

WatchController::manifest()

public function manifest(string $slug, RemoteManifest $remote): Response

A 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): Response

The player's beacon: start, progress, end (Service\Views).

WatchController::rate()

public function rate(Request $request, string $slug, RateLimiterFactoryInterface $limiter): Response

WatchController::comment()

public function comment(Request $request, string $slug, Comments $comments, CommentGuard $guard, RateLimiterFactoryInterface $limiter): Response

WatchController::report()

public function report(Request $request, string $slug, Moderation $moderation, RateLimiterFactoryInterface $limiter): Response

WatchController::source()

public static function source(Request $request): string

Where 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(): iterable

Base\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(): TreeBuilder

VideoExtension class

class VideoExtension extends AbstractBaseExtension implements PrependExtensionInterface VideoExtension.php:12

VideoExtension::getConfiguration()

public function getConfiguration(array $config, ContainerBuilder $container): VideoConfiguration

VideoExtension::prepend()

public function prepend(ContainerBuilder $container): void

The 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): void

Base\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): string

Channel::__toString()

public function __toString(): string

Channel::getId()

public function getId(): ?int

Channel::getSlug()

public function getSlug(): ?string

Channel::setSlug()

public function setSlug(?string $slug): self

Channel::getName()

public function getName(): ?string

Channel::setName()

public function setName(?string $name): self

Channel::getDescription()

public function getDescription(): ?string

Channel::setDescription()

public function setDescription(?string $description): self

Channel::getOwners()

public function getOwners(): Collection

Channel::getOwner()

public function getOwner(): ?User

Channel::addOwner()

public function addOwner(User $owner): self

Channel::removeOwner()

public function removeOwner(User $owner): self

Channel::isOwnedBy()

public function isOwnedBy(?User $user): bool

Channel::getAvatar()

public function getAvatar(): ?string

Channel::setAvatar()

public function setAvatar(?string $avatar): self

Channel::getBanner()

public function getBanner(): ?string

Channel::setBanner()

public function setBanner(?string $banner): self

Channel::getShelves()

public function getShelves(): array

Channel::setShelves()

public function setShelves(array $shelves): self

Channel::getSubscribers()

public function getSubscribers(): int

Channel::setSubscribers()

public function setSubscribers(int $subscribers): self

Channel::getCreatedAt()

public function getCreatedAt(): ?\DateTimeInterface

Channel::getVideos()

public function getVideos(): Collection

Channel::getInitials()

public function getInitials(): string

Two 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(): string

Playlist::getId()

public function getId(): ?int

Playlist::getSlug()

public function getSlug(): string

Playlist::getTitle()

public function getTitle(): ?string

Playlist::setTitle()

public function setTitle(?string $title): self

Playlist::getDescription()

public function getDescription(): ?string

Playlist::setDescription()

public function setDescription(?string $description): self

Playlist::getOwner()

public function getOwner(): User

Playlist::isOwnedBy()

public function isOwnedBy(?User $user): bool

Playlist::getKind()

public function getKind(): string

Playlist::isLater()

public function isLater(): bool

Playlist::getVisibility()

public function getVisibility(): string

Playlist::setVisibility()

public function setVisibility(string $visibility): self

Playlist::isPublic()

public function isPublic(): bool

Playlist::isReachableBy()

public function isReachableBy(?User $user): bool

Playlist::getItems()

public function getItems(): Collection

Playlist::getVideos()

public function getVideos(): array
Returns

the videos still reachable, in order

Playlist::has()

public function has(Video $video): bool

Playlist::find()

public function find(Video $video): ?PlaylistItem

Playlist::add()

public function add(Video $video): self

At the end ("plus tard": at the start, the newest first).

Playlist::remove()

public function remove(Video $video): self

Playlist::reorder()

public function reorder(array $videoIds): self
$videoIds

the new order, by video id; the others keep theirs after

Playlist::getFirstVideo()

public function getFirstVideo(): ?Video

Playlist::next()

public function next(Video $video): ?Video

The one after $video in the list, for "à suivre".

Playlist::getCreatedAt()

public function getCreatedAt(): \DateTimeInterface

Playlist::getUpdatedAt()

public function getUpdatedAt(): \DateTimeInterface

PlaylistItem class

class PlaylistItem PlaylistItem.php:7

PlaylistItem::__construct()

public function __construct(Playlist $playlist, Video $video, int $position = 0)

PlaylistItem::getId()

public function getId(): ?int

PlaylistItem::getPlaylist()

public function getPlaylist(): Playlist

PlaylistItem::getVideo()

public function getVideo(): Video

PlaylistItem::getPosition()

public function getPosition(): int

PlaylistItem::setPosition()

public function setPosition(int $position): self

PlaylistItem::getAddedAt()

public function getAddedAt(): \DateTimeInterface

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.

public function __construct(Video $video, Video $related, float $score)
public function getId(): ?int
public function getVideo(): Video
public function getRelated(): Video
public function getScore(): float
public function getComputedAt(): \DateTimeInterface

Source 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(): string

Source::getId()

public function getId(): ?int

Source::getVideo()

public function getVideo(): ?Video

Source::setVideo()

public function setVideo(?Video $video): self

Source::getKind()

public function getKind(): string

Source::getRole()

public function getRole(): string

Source::getUrl()

public function getUrl(): string

Source::setUrl()

public function setUrl(string $url): self

Source::isAbsolute()

public function isAbsolute(): bool

Source::getPlatform()

public function getPlatform(): ?string

Source::setPlatform()

public function setPlatform(?string $platform): self

Source::getExternalId()

public function getExternalId(): ?string

Source::setExternalId()

public function setExternalId(?string $externalId): self

Source::setReference()

public function setReference(?string $platform, ?string $frame): self

The reference of an outside film: [platform, id], from what embed_url() frames ("https://player.vimeo.com/video/76979871").

Source::getWidth()

public function getWidth(): ?int

Source::getHeight()

public function getHeight(): ?int

Source::setSize()

public function setSize(?int $width, ?int $height): self

Source::getBitrate()

public function getBitrate(): ?int

Source::setBitrate()

public function setBitrate(?int $bitrate): self

Source::getCodecs()

public function getCodecs(): ?string

Source::setCodecs()

public function setCodecs(?string $codecs): self

Source::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(): ?int

Subscription::getUser()

public function getUser(): User

Subscription::getChannel()

public function getChannel(): Channel

Subscription::getCreatedAt()

public function getCreatedAt(): \DateTimeInterface

Video 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(): ?array
public function __toLink(array $routeParameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): ?string

Video::__typesense()

public function __typesense(): ?string

Video::__construct()

public function __construct(?User $owner = null, ?Thread $parent = null, ?string $title = null, ?string $slug = null)

Video::__toString()

public function __toString(): string

Video::getChannel()

public function getChannel(): ?Channel

Video::setChannel()

public function setChannel(?Channel $channel): self

Video::getSources()

public function getSources(): Collection

Video::addSource()

public function addSource(Source $source): self

Video::removeSource()

public function removeSource(Source $source): self

Video::clearSources()

public function clearSources(): self

Video::getPlayableSource()

public function getPlayableSource(): ?Source

The 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(): bool

Video::isRemote()

public function isRemote(): bool

Video::isPlayable()

public function isPlayable(): bool

Video::getProcessing()

public function getProcessing(): string

Video::setProcessing()

public function setProcessing(string $processing): self

Video::isProcessing()

public function isProcessing(): bool

Video::hasFailed()

public function hasFailed(): bool

Video::getFailure()

public function getFailure(): ?string

Video::setFailure()

public function setFailure(?string $failure): self

Video::getDuration()

public function getDuration(): ?int

Video::setDuration()

public function setDuration(?int $duration): self

Video::getDurationText()

public function getDurationText(): string

1:02:03, 4:05, 0:07.

Video::formatTime()

public static function formatTime(?int $seconds): string

Video::getWidth()

public function getWidth(): ?int

Video::getHeight()

public function getHeight(): ?int

Video::setSize()

public function setSize(?int $width, ?int $height): self

Video::getRatio()

public function getRatio(): string

"16 / 9", for the player's frame before anything loads.

Video::getPoster()

public function getPoster(): ?string

Video::setPoster()

public function setPoster(?string $poster): self

Video::getStoryboard()

public function getStoryboard(): ?string

Video::setStoryboard()

public function setStoryboard(?string $storyboard): self

Video::getStorageKey()

public function getStorageKey(): ?string

Video::setStorageKey()

public function setStorageKey(?string $storageKey): self

Video::getUploadId()

public function getUploadId(): ?string

Video::setUploadId()

public function setUploadId(?string $uploadId): self

Video::getUploadSize()

public function getUploadSize(): int

Video::setUploadSize()

public function setUploadSize(?int $uploadSize): self

Video::getOriginalName()

public function getOriginalName(): ?string

Video::setOriginalName()

public function setOriginalName(?string $originalName): self

Video::getViews()

public function getViews(): int

Video::setViews()

public function setViews(int $views): self

Video::getLegacyViews()

public function getLegacyViews(): ?int

Video::setLegacyViews()

public function setLegacyViews(?int $legacyViews): self

Video::getTotalViews()

public function getTotalViews(): int

Ours and the imported ones: what a card shows.

Video::getCategory()

public function getCategory(): ?string

Video::setCategory()

public function setCategory(?string $category): self

Video::getLanguage()

public function getLanguage(): ?string

Video::setLanguage()

public function setLanguage(?string $language): self

Video::getLicense()

public function getLicense(): string

Video::setLicense()

public function setLicense(?string $license): self

Video::areCommentsEnabled()

public function areCommentsEnabled(): bool

Video::isCommentsEnabled()

public function isCommentsEnabled(): bool

Video::setCommentsEnabled()

public function setCommentsEnabled(bool $commentsEnabled): self

Video::getExternalUrl()

public function getExternalUrl(): ?string

Video::setExternalUrl()

public function setExternalUrl(?string $externalUrl): self

Video::getTakedownReason()

public function getTakedownReason(): ?string

Video::getTakenDownAt()

public function getTakenDownAt(): ?\DateTimeInterface

Video::isTakenDown()

public function isTakenDown(): bool

Video::takeDown()

public function takeDown(string $reason): self

Out of sight, the reason kept for its author; restore() puts it back as it was chosen (private).

Video::restore()

public function restore(): self

Video::getVisibility()

public function getVisibility(): string

Video::setVisibility()

public function setVisibility(string $visibility, ?\DateTimeInterface $at = null): self

Public, 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(): bool

Listed on the platform: in the feeds, the search, the suggestions.

Video::isReachable()

public function isReachable(): bool

Reachable by its address (listed, or unlisted).

Video::isOwnedBy()

public function isOwnedBy(?User $user): bool

Video::getSearchTitle()

public function getSearchTitle(): string

Video::getSearchText()

public function getSearchText(): string

Video::getSearchChannel()

public function getSearchChannel(): string

Video::getSearchChannelSlug()

public function getSearchChannelSlug(): string

Video::getSearchCategory()

public function getSearchCategory(): string

Video::getSearchDuration()

public function getSearchDuration(): int

Video::getSearchViews()

public function getSearchViews(): int

Video::getSearchPublished()

public function getSearchPublished(): int

Video::getSearchListed()

public function getSearchListed(): bool

Video::getSearchTags()

public function getSearchTags(): array

VideoComment 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(): ?int

VideoComment::setMoment()

public function setMoment(?int $moment): static

VideoComment::getMomentText()

public function getMomentText(): string

VideoComment::isPinned()

public function isPinned(): bool

VideoComment::setPinned()

public function setPinned(bool $pinned): static

VideoComment::getVideo()

public function getVideo(): ?Video

VideoComment::timecodes()

public static function timecodes(string $text): array

The 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
$m

a 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(): ?int

WatchEvent::getVideo()

public function getVideo(): Video

WatchEvent::getUser()

public function getUser(): ?User

WatchEvent::getToken()

public function getToken(): string

WatchEvent::getVisitor()

public function getVisitor(): string

WatchEvent::getSource()

public function getSource(): string

WatchEvent::getReferrer()

public function getReferrer(): ?string

WatchEvent::getPosition()

public function getPosition(): float

WatchEvent::getWatched()

public function getWatched(): float

WatchEvent::progress()

public function progress(float $position, float $watched, ?\DateTimeInterface $now = null): self

What 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(): bool

WatchEvent::count()

public function count(): self

WatchEvent::isFlushed()

public function isFlushed(): bool

WatchEvent::flush()

public function flush(): self

WatchEvent::isCompleted()

public function isCompleted(): bool

WatchEvent::complete()

public function complete(): self

WatchEvent::getStartedAt()

public function getStartedAt(): \DateTimeInterface

WatchEvent::getUpdatedAt()

public function getUpdatedAt(): \DateTimeInterface

Base\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): self

Base\Video\Form

ChannelDetailsType class

class ChannelDetailsType extends AbstractType ChannelDetailsType.php:15

ChannelDetailsType::buildForm()

public function buildForm(FormBuilderInterface $builder, array $options): void

ChannelDetailsType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

LinkDetailsType class

class LinkDetailsType extends AbstractType LinkDetailsType.php:12

LinkDetailsType::buildForm()

public function buildForm(FormBuilderInterface $builder, array $options): void

LinkDetailsType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

PlaylistDetailsType class

class PlaylistDetailsType extends AbstractType PlaylistDetailsType.php:14

PlaylistDetailsType::buildForm()

public function buildForm(FormBuilderInterface $builder, array $options): void

PlaylistDetailsType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

VideoDetailsType 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): void

VideoDetailsType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

Base\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 $type
public readonly string $userId
public readonly int $videoId
public 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 $videoId
public 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): void

FlushViewsHandler class

final class FlushViewsHandler FlushViewsHandler.php:9

FlushViewsHandler::__construct()

public function __construct(readonly Views $views)

FlushViewsHandler::__invoke()

public function __invoke(FlushViews $message): void

SuggestFeedbackHandler 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): void

TranscodeVideoHandler 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): void

Base\Video\Model

ChannelDetails class

class ChannelDetails ChannelDetails.php:9

public ?string $name = null
public ?string $slug = null
public ?string $description = null
public array $shelves = Channel::SHELVES
public ?UploadedFile $avatarFile = null
public ?UploadedFile $bannerFile = null

ChannelDetails::of()

public static function of(Channel $channel): self

LinkDetails class

class LinkDetails LinkDetails.php:8

A film of another platform, added by its address (framed through embed_url()).

public ?string $url = null
public ?string $title = null

PlaylistDetails class

class PlaylistDetails PlaylistDetails.php:8

public ?string $title = null
public ?string $description = null
public string $visibility = Playlist::PRIVATE

PlaylistDetails::of()

public static function of(Playlist $playlist): self

VideoDetails 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 = null
public ?string $description = null
public ?string $category = null
public ?string $tags = null

Keywords, separated by commas.

public ?string $language = null
public string $license = 'standard'
public string $visibility = Video::VISIBILITY_PRIVATE
public ?\DateTimeInterface $publishAt = null
public bool $commentsEnabled = true
public ?string $poster = null

One of the stills, chosen as the poster (its address).

public ?UploadedFile $posterFile = null

VideoDetails::of()

public static function of(Video $video): self

VideoDetails::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): ?Channel

The first channel a member owns.

ChannelRepository::freeSlug()

public function freeSlug(string $name): string

A 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): void

RelatedRepository::deleteAll()

public function deleteAll(): void

SourceRepository 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): ?Subscription

SubscriptionRepository::countFor()

public function countFor(Channel $channel): int

SubscriptionRepository::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): int

VideoCommentRepository::findForVideo()

public function findForVideo(Video $video): array
Returns

every comment of a film, for its author's studio

VideoCommentRepository::countPending()

public function countPending(): int

VideoCommentRepository::unpinAll()

public function unpinAll(Video $video): void

VideoRepository 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'): QueryBuilder

Listed: 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): ?Video

The video at that address, if its visitor may see it (listed, unlisted, or theirs).

VideoRepository::findOneByUploadId()

public function findOneByUploadId(string $uploadId): ?Video

VideoRepository::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): int

Bytes 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): array

The 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): QueryBuilder

VideoRepository::durationBounds()

public static function durationBounds(?string $duration): array

short: under 4 minutes; medium: 4 to 20; long: over 20.

@return array{0: ?int, 1: ?int}

VideoRepository::dateSince()

public static function dateSince(?string $date): ?\DateTime

VideoRepository::countByProcessing()

public function countByProcessing(): array
Returns

processing state => videos, for the back office

VideoRepository::findProcessing()

public function findProcessing(int $limit = 10): array

VideoRepository::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): ?WatchEvent

WatchEventRepository::findHistory()

public function findHistory(User $user, int $limit = 60): array

A 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): ?float

Where a member stopped last time, in seconds (null: never, or near the end).

WatchEventRepository::clearHistory()

public function clearHistory(User $user): int

WatchEventRepository::removeFromHistory()

public function removeFromHistory(User $user, Video $video): int

WatchEventRepository::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): int

WatchEventRepository::findCoWatching()

public function findCoWatching(int $days = 90, int $limit = 200000): array

Who 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): array

The 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): array

The 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): int

Counted views over the last $days days, all videos: the dashboard's figure.

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): void

SafeIndexer::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)
public function search(string $term, array $filters = [], int $page = 1): array

VideoSearch::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): ?Channel

Channels::isSubscribed()

public function isSubscribed(?User $user, Channel $channel): bool

Channels::toggle()

public function toggle(User $user, Channel $channel): bool

Follow, 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): VideoComment

Comments::pin()

public function pin(VideoComment $comment, bool $pinned = true): void

Pinned first under the film (one at a time), or unpinned.

Comments::hide()

public function hide(VideoComment $comment, bool $hidden = true): void

Hidden 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): void
public function link(LinkDetails $details, User $user): ?Video

A film of another platform: its page, framed through embed_url(); null when nothing can frame it.

Editor::tags()

public function tags(array $keywords): array

The 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 = ''): Complaint

Moderation::takeDown()

public function takeDown(Video $video, string $reason): void

Moderation::restore()

public function restore(Video $video): void

Moderation::subjectOf()

public function subjectOf(Complaint $complaint): Video|VideoComment|Channel|null

The 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): void

Notifier::failed()

public function failed(Video $video): void

Notifier::commented()

public function commented(VideoComment $comment): void

Notifier::takenDown()

public function takenDown(Video $video): void

Pictures 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): string

Pictures::channel()

public function channel(Channel $channel, UploadedFile $file, string $kind): string

Processing 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): bool

Processing::frames()

public function frames(Video $video): array
Returns

the stills a poster can be chosen from

Processing::removeUpload()

public function removeUpload(string $source): void

tusd'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): array

Ratings::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): bool

RemoteManifest::master()

public function master(string $url): string

The master playlist at $url, its URIs absolute.

RemoteManifest::duration()

public function duration(string $url): ?int

The film's length: the first variant's #EXTINF summed (null when unreadable).

RemoteManifest::absolutize()

public static function absolutize(string $playlist, string $base): string

Every 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): int

Seconds, the #EXTINF of a media playlist summed.

RemoteManifest::resolve()

public static function resolve(string $uri, string $base): string

Storage 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(): string

Storage::directory()

public function directory(Video $video, bool $create = true): string

The video's directory, its key made the first time.

Storage::workspace()

public function workspace(Video $video): string

A fresh directory beside it, to build a new ladder before it replaces the old one.

Storage::promote()

public function promote(Video $video, string $workspace): void

The workspace becomes the video's directory; the previous files go.

Storage::discard()

public function discard(string $workspace): void

Storage::url()

public function url(Video $video, string $file): string

The address a file of the video's directory is served at.

Storage::pathOf()

public function pathOf(string $url): ?string

The file behind one of our addresses, or null for anything else.

Storage::remove()

public function remove(Video $video): void

Storage::size()

public function size(Video $video): int

Bytes 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 = 160
const 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): array

Everything for one film, written into $directory.

Transcoder::ladder()

public function ladder(int $width, int $height): array

The 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): array

ffmpeg'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): ?array

The 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): array

Transcoder::storyboardVtt()

public static function storyboardVtt(float $duration, array $board, string $sprite): string

The 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): array

What ffprobe measures: length, picture size (turned when the film is), sound, weight.

Transcoder::measures()

public static function measures(array $json, string $path = ''): array

Transcoder::bitrate()

public static function bitrate(int $height): array

Transcoder::even()

public static function even(int|float $value): int

The nearest even number (853.3 -> 854): x264 wants even sides.

Transcoder::vttTime()

public static function vttTime(float $seconds): string

00: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(): bool

Uploads::token()

public function token(User $user, ?Video $replace = null, ?int $now = null): string

A 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): ?int

Bytes $user may still send (null: no quota).

Uploads::refusal()

public function refusal(?User $user, int $size, bool $consume = false): ?string

Why $user may not send a file of $size bytes now, or null.

Uploads::hook()

public function hook(array $hook): array

tusd'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): ?Video

The video made of a finished upload (or the one whose file it replaces), its transcoding queued.

Uploads::slug()

public static function slug(): string

A short random address part, already what omnibase's Slugify keeps (lowercase): /v/3f9a0c71be42.

Uploads::isUploadPath()

public function isUploadPath(string $path): bool

Inside 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): float

Seconds of playing that make a view of this film.

Views::visitor()

public function visitor(Request $request, ?User $user = null): string

Views::record()

public function record(Video $video, array $beacon, string $visitor, ?User $user = null, ?\DateTimeInterface $now = null): ?WatchEvent

A 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(): int

The 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): void

Base\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(): bool

GorseSuggest::feedback()

public function feedback(string $type, User $user, Video $video): void

GorseSuggest::send()

public function send(string $type, string $userId, int $videoId, ?\DateTimeInterface $at = null): void

GorseSuggest::recommend()

public function recommend(User $user, int $limit = 24): array

GorseSuggest::session()

public function session(array $videoIds, int $limit = 24): array

GorseSuggest::similar()

public function similar(Video $video, int $limit = 12): array

GorseSuggest::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): void

LocalSuggest::recommend()

public function recommend(User $user, int $limit = 24): array

LocalSuggest::session()

public function session(array $videoIds, int $limit = 24): array

LocalSuggest::similar()

public function similar(Video $video, int $limit = 12): array

LocalSuggest::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.

const NEIGHBOURS = 24
const MAX_GROUP = 400

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): array

The scoring itself, on plain data - tested without a database.

$groups

video id => group => weight

$audiences

video 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): float

RelatedCalculator::groups()

public function groups(array $videos): array

The 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): void

A 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
$videoIds

what 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(): bool

Suggestions::feedback()

public function feedback(string $type, User $user, Video $video): void

Suggestions::recommend()

public function recommend(User $user, int $limit = 24): array

Suggestions::session()

public function session(array $videoIds, int $limit = 24): array

Suggestions::similar()

public function similar(Video $video, int $limit = 12): array

Base\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(): array

VideoExtension::getFunctions()

public function getFunctions(): array

VideoExtension::timecodes()

public function timecodes(?string $text, ?Video $video = null): string

The 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): string

The 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): string

VideoExtension::ago()

public function ago(?\DateTimeInterface $date, ?\DateTimeInterface $now = null): string

VideoExtension::rating()

public function rating(Video $video): array

VideoExtension::subscribed()

public function subscribed(Channel $channel): bool

VideoExtension::uploadToken()

public function uploadToken(?Video $replace = null): ?string