Skip to content

Product Video Management (Admin)

Flow ID: AD-30 Module(s): video, eshop, job Complexity: High Last Updated: 2026-04-04

Business Context

Two independent video subsystems serve different purposes. YouTube Videos (v1) store YouTube IDs with auto-downloaded thumbnails, linked to products and vendors via lookup tables. Video Showcase / Stream (v2) provides self-hosted video with Bunny.net CDN integration, file uploads (video + thumbnail + preview), async processing via job queue, view metrics tracking, and product assignment with ordering. The Showcase subsystem powers the "Reels" storefront page and the home page video slider. Both subsystems are managed from the admin panel under the CONTENT menu group.

Architecture

Legacy Admin (video module)
  ├── Adv_video_admin.php        → YouTube video CRUD (199 lines)
  ├── AdvVideoShowcaseAdmin.php  → Bunny Stream CRUD (405 lines)
  ├── Adv_video_model.php        → YouTube model: tables video, video_mui, lookup tables
  └── AdvVideoShowcaseModel.php  → Stream model: tables video_streams, video_streams_mui, video_streams_lp

Modern Domain Layer (src/Domains/Cms/Video/)
  ├── Repository/Entity.php      → YouTube video entity (video table)
  ├── Repository/Repository.php  → Read repo with Specification pattern
  ├── Repository/RepositoryConfigurator.php → Relations: translations, products, vendors
  ├── Repository/MuiEntity.php   → Translation entity (video_mui)
  ├── Repository/MuiRepository.php
  ├── Service.php                → Read service (list, item, get)
  ├── WriteService.php           → CRUD with transactional MUI handling
  ├── WriteData.php / MuiWriteData.php → DTOs with OpenAPI schemas
  ├── Validator.php              → Validation (lang required per translation)
  └── ListRequest.php            → Filters: id, youtubeVideoId, isPromo, name.{locale}, slug.{locale}

REST API (src/Rest/Cms/)
  └── Controllers/Video.php      → Full CRUD + file upload (thumb field)
      Resources/Video/Resource.php, Collection.php, MuiResource.php, MuiCollection.php

VideoStream Integration (src/VideoStream/)
  ├── Contract/VideoStream.php   → Interface: createVideo, uploadVideo, getVideo, deleteVideo, getUrls, getStatus
  ├── VideoManager.php           → Orchestrator: createAndUpload, getUrls, getVideoStatus, deleteVideo
  └── Stream/BunnyStream.php     → Bunny.net CDN implementation (Guzzle HTTP, library-scoped API)

Job Queue (ecommercen/job/libraries/)
  ├── AdvUploadVideoToStream.php      → Upload local file to Bunny CDN, then chain CheckVideoStatusStream
  ├── AdvDeleteVideoToStream.php      → Delete video from Bunny CDN by cdn_video_id
  └── AdvCheckVideoStatusStream.php   → Poll Bunny API for encoding status, re-queue if not FINISHED/ERROR

Database Schema

YouTube Videos (v1)

TableColumnsNotes
videoid, youtube_video_id (unique), custom_datetime, thumb, is_promoMaster record
video_muiid, video_id, name, description, slug, langMUI with slug+lang index
shop_product_video_lpid, product_id, video_idProduct-video many-to-many
shop_vendor_video_lpid, video_id, vendor_idVendor-video many-to-many

Video Showcase / Stream (v2)

TableColumnsNotes
video_streamsid, cdn_video_id, cdn_video_uri, cdn_video_thumbnail, custom_thumbnail, custom_preview, is_promo, active, status, created_at, updated_atMigration: 20250328114125_video_stream_data.php
video_streams_muiid, video_stream_id, name, langName-only MUI (no slug/description)
video_streams_lpid, video_stream_id, product_id, orderOrdered product assignment
view_metricsobject_type ("stream"), object_id, since, expires_after, hits, impressionsView counting for stream videos

Key Features

YouTube Video Subsystem (v1)

  • YouTube ID validation: is_valid_youtube_id + getYoutubeId custom CI form validation rules; uniqueness enforced in DB and on form submit.
  • Auto-thumbnail download: On add/update, getYoutubeThumb() tries 3 quality levels from img.youtube.com/vi/{id}/{0,1,2}.jpg, saves first successful one via storage()->put().
  • MUI: Name, description, slug (auto-generated via createSlug()) per language.
  • Product assignment: Batch assign via assignMultipleProductsToVideo() with deduplication against existing DB relations. Product admin uses assign_products_to_video batch operation.
  • Vendor assignment: Replace-all strategy via assignMultipleVideosToVendor() (deletes existing, re-inserts).
  • Product-level YouTube embed: Each product has a per-language video field in shop_product_mui storing a YouTube ID, rendered in AdvProductGalleryWithVideo.vue as an embedded iframe.

Video Showcase / Stream Subsystem (v2)

  • Feature gate: Registry key VIDEOSHOWCASE.ENABLED must be truthy; controller returns 404 otherwise. Admin menu item conditionally shown.
  • File uploads: Three file fields handled by AdvUploader:
    • video_file: up to 200 MB, types mp4|avi|wmv|mov|gif|webp, stored in files/videos/videofiles/
    • thumbnail_image: up to 2 MB, standard image types, stored in files/videos/
    • video_preview_file: up to 100 MB, types gif|webp, stored in files/videos/previews/
  • Bunny CDN pipeline: On add, afterAdd() enqueues UploadVideoToStream job (queue: fileManager, 3 retries, 300s grace). The job uploads the local file via VideoManager::createAndUpload(), stores the returned GUID and thumbnail URL, sets status to UPLOADED, then chains CheckVideoStatusStream. The status checker polls BunnyStream::getStatus() and re-queues itself until the status reaches FINISHED or ERROR.
  • Status lifecycle: CREATED -> UPLOADED -> FINISHED (or ERROR). Bunny API statuses 0=CREATED, 1/2/3=UPLOADED (encoding), 4=FINISHED, 5-8=ERROR.
  • On edit with new video: If a new video file is uploaded and the record already has a cdn_video_id, afterEdit() enqueues DeleteVideoToStream (to remove the old CDN video) followed by UploadVideoToStream (to upload the new one).
  • On delete: Removes from video_streams, video_streams_mui, video_streams_lp, and view_metrics. Enqueues DeleteVideoToStream if a cdn_video_id exists.
  • Product assignment: JSON array of {product_id, order} objects posted as selected_products. Product search endpoint (search_stream_products) searches by product name (LIKE), product ID (exact), product code (exact), or barcode (exact), returns up to 50 results with product code images.
  • View metrics: record_view() endpoint records hits via view_metrics_model::recordMetrics(). Listing page shows total hits per video formatted via format_stream_views().
  • Admin search: Session-persisted search by video name (LIKE) or cdn_video_id (LIKE).

Admin Controllers

ControllerLinesSubsystemKey Methods
Adv_video_admin.php199YouTubeindex($offset), add(), edit($id), delete($videoId)
AdvVideoShowcaseAdmin.php405Streamindex($offset), add(), edit($id), delete($videoId), search_stream_products(), get_video_status($id), record_view()

REST API (YouTube Videos only)

The modern REST layer covers the YouTube video entity (video table) with full CRUD + file upload support.

MethodEndpointActionAuth
GET/rest/cms/videoList with filters, sorts, paginationGuest
GET/rest/cms/video/{id}Single by IDGuest
GET/rest/cms/video/itemSingle by filterGuest
POST/rest/cms/videoCreate (JSON or multipart)Backend: ADMIN, MEDIA
POST/rest/cms/video/{id}Update (JSON or multipart)Backend: ADMIN, MEDIA
DELETE/rest/cms/video/{id}DeleteBackend: ADMIN, MEDIA

Relations: translations (one-to-many), products (many-to-many via shop_product_video_lp), vendors (many-to-many via shop_vendor_video_lp).

Upload field: thumb -> files/videos/.

Routes

Admin (Legacy):

  • video_admin / video_admin/(.+) -> video/video_admin
  • video_showcase_admin / video/video_showcase_admin -> video/video_showcase_admin
  • video/video_showcase_admin/search_stream_products -> product search API

Storefront (Stream):

  • reels -> eshop/reels/index (paginated Reels page)
  • api/reels -> eshop/reels/getPaginatedVideos (JSON pagination endpoint)

Configuration

SettingLocationDescription
VIDEOSHOWCASE.ENABLEDRegistryFeature gate for Stream subsystem
VIDEOSHOWCASE.PROVIDERRegistryProvider string (currently only BUNNY)
BUNNY.BUNNY_API_KEYRegistry (encrypted)Bunny.net API key
BUNNY.BUNNY_LIBRARY_IDRegistryBunny video library ID
BUNNY.BUNNY_CDN_HOSTNAMERegistryCDN hostname for URL generation
streamAllowedTypesapp.phpmp4|avi|wmv|mov|gif|webp
streamPreviewAllowedTypesListapp.phpgif|webp

Settings page: settings/videoShowcase (controller: Adv_settings::videoShowcase()). Validates Bunny credentials are provided when BUNNY is selected as provider.

Admin Menu

Under CONTENT group, parent "Video" with two children:

  • Videos (video_admin) -- YouTube video management. Roles: ADVISABLE, ADMIN, CMS, MEDIA.
  • Video Showcase (video_showcase_admin) -- Stream video management. Roles: ADVISABLE, ADMIN, CMS, MEDIA. Conditionally displayed based on VIDEOSHOWCASE.ENABLED.

Authorization

LayerRoles
YouTube admin (Adv_video_admin)ADVISABLE, ADMIN, MEDIA
Stream admin (AdvVideoShowcaseAdmin)ADVISABLE, ADMIN, MEDIA
REST API write (rest_policies.php)ADMIN, MEDIA (backend auth)
REST API readGuest (no auth)

Business Rules

RuleDescription
YouTube ID uniquenessEnforced via is_unique form validation rule on video.youtube_video_id, excluding current record on update
Feature gateStream subsystem returns 404 if VIDEOSHOWCASE.ENABLED is falsy
Async CDN uploadVideo file uploaded locally first, then pushed to Bunny CDN asynchronously via job queue
Status pollingCheckVideoStatusStream re-queues itself until Bunny reports FINISHED or ERROR
CDN replacementOn video file replacement, old CDN video deleted before new one uploaded
Cascade delete (Stream)Removes stream record, MUI, product lookup, view metrics, and enqueues CDN deletion
Home page displayStream videos shown on home page if active=1, is_promo=1, status=FINISHED, and cdn_video_id IS NOT NULL
Product search (Stream)Searches by name (LIKE), product ID (exact), product code (exact), or barcode (exact); excludes soft-deleted products

Storefront Integration

  • Home page: Adv_home::getStreamVideos() fetches up to 8 promo/active/finished stream videos with their assigned products, enriches with CDN URLs, and passes to the template as homeStreamSliderData.
  • Reels page: Adv_reels controller provides server-rendered initial page + AJAX pagination (getPaginatedVideos) for the full stream video catalog. Products enriched with live pricing, stock, and image data. All video products collected for structured data output.
  • Product page YouTube embed: AdvProductGalleryWithVideo.vue renders a YouTube iframe (youtube.com/embed/{id}) when the product's MUI video field is populated. Responsive 16:9 aspect ratio.
  • Vendor page: Vendors can have YouTube videos assigned via shop_vendor_video_lp, displayed on vendor detail pages.

Video Player Library

The storefront video player uses video.js (video.js npm package) in assets/main/vue/streamSlider/VideoPlayer.vue for HLS playback of Bunny CDN streams. Video.js handles adaptive bitrate streaming, playback controls, and responsive sizing for the Reels page and home page video slider.