This reference covers the current Video Tracking runtime: module bootstrap, canonical storage, AJAX/REST entry points, Bricks integration, and the services that track watch state.
Overview
The Video Tracking runtime lives under src/Modules/VideoWatch/. It adds tracked video playback to BRM content, hosted video selection, and a native brm-video Bricks element.
- Admin surface —
BricksMembers → Video Trackingwith General, Analytics, and Video Hosts tabs for global defaults, optional reporting/automation storage, media protection, and host connections - Editor surface — BRM meta box fields for
brm_video_url,brm_video_duration,brm_video_image,brm_video_title,brm_video_chapters,brm_video_text_tracks, and per-post watch policy overrides - Frontend surface —
BRM VideoBricks element plus watch-state AJAX sync and dynamic tags
Core Services
- VideoWatchRuntime — Module bootstrap. Registers cross-module hooks and BRM Video element refresh controls.
- VideoConfigResolver — Resolves effective config for a post by merging global defaults, per-post policy, and the resolved video source.
- VideoWatchService — Canonical read/write service for user watch state. Exposes
get_watch_state(),get_effective_watch_state(),sync(), and related context helpers. - VideoWatchSessionTokenService — Since version 1.3.0, issues and verifies signed per-render watch tokens for logged-in playable BRM Video renders. Tokens are bound to user ID, post ID, source hash, and expiry.
- VideoPlaybackAccessService — Since version 1.3.0, evaluates optional BRM Video element playback gates. Playback is allowed by default; gate checks run only when the element opts into logged-in, selected-gate, or all-gates playback access.
- VideoSourceService — Canonical read/write service for
_brm_video_source, provider hints, effective source resolution, and related runtime invalidation. - VideoProviderRegistry — Detects the provider from raw URL/ID input and normalizes provider-specific source data.
- VideoPickerService — Lists videos and groups from connected providers for admin picker flows and exposes
get_connected_providers()for editor surfaces. - VideoRefreshService — Adds threshold-reached refresh controls to the
brm-videoelement and enqueues the frontend refresh handler when needed. - VideoTimedActionService — Since version 1.3.0, owns timed-action BRM Video element controls and render config for partial refresh, full refresh, redirect, reporting, and automation handoff.
- VideoActionLedgerService — Since version 1.3.0, owns optional durable timed-action state, daily action projections, action rollups, and automation dispatch handoff.
- VideoAnalyticsService — Since version 1.3.0, owns optional daily video analytics projections and aggregate video/action reporting reads. Reports read daily rollups when enabled instead of scanning raw watch/action rows.
- VideoTextTrackService — Since version 1.3.0, normalizes saved text tracks and returns transcript cues for a canonical source after token/source-hash validation.
- VideoMediaUrlSigningService — Since version 1.3.0, applies render-only provider-supported media URL signing for Bunny and Cloudflare Stream. It does not proxy media bytes through PHP and does not change the canonical source hash.
Canonical Storage
Video Tracking uses a mix of options, post meta, and a dedicated watch-state table.
brm_video_watch_settings— Global defaults such as required percent, completion mode, gating, resume mode, seek policy, thumbnail settings, optional analytics/action reporting flags, and media URL protection settingsbrm_video_host_credentials— Provider credentials and access tokens for the video pickerbrm_video_host_enabled— Per-provider admin enable flags_brm_video_source— Canonical normalized source payload for a post_brm_video_source_provider— Optional provider hint stored alongside the authoredbrm_video_urlvalue so opaque IDs can resolve deterministically_brm_video_watch_policy— Per-post overrides for required percent, completion mode, and next-navigation gatingbrm_video_url,brm_video_duration,brm_video_image,brm_video_title,brm_video_chapters,brm_video_text_tracks— Authored BRM Video editor fields kept in sync with the normalized source projectionbrm_video_durationis also used by scoped reading-time totals when the caller opts into video duration. The Bricks tag form is{brm_post:reading_time:toplevel:minutes:with_video}; the PHP read path isStructureReadingTimeReadServiceand the public wrappers accept$include_video_duration = true. This path uses saved metadata only and must not call provider APIs during render.{prefix}brm_user_video_watch— Per-user/per-post watch state including coverage, watched buckets, threshold flags, completion flags, duration, and last/max positions{prefix}brm_video_viewer_dailyand{prefix}brm_video_analytics_daily— Since version 1.3.0, optional analytics projection and daily rollup tables created only when video analytics reporting is enabled or repaired by a schema maintenance path{prefix}brm_user_video_action_state,{prefix}brm_video_action_user_daily, and{prefix}brm_video_action_daily— Since version 1.3.0, optional durable timed-action state and reporting rollup tables created only when action tracking/reporting/automation storage requires them
VideoWatchService treats the watch table as the canonical runtime store for learner watch progress. The current row shape includes values such as coverage_map, watched_buckets, watched_percent, threshold_percent, threshold_reached, fully_watched, last_position_seconds, max_position_seconds, duration_seconds, and source_hash.
Effective Watch State vs Post Watch State
VideoWatchService::get_effective_watch_state() is the preferred read path for frontend logic. It can aggregate related child video posts when a lesson/template context should behave like a single tracked lesson rather than an isolated child video post.
$state = \BaselMedia\BricksMembers\Modules\VideoWatch\VideoWatchService::get_instance()
->get_effective_watch_state( get_current_user_id(), get_the_ID() );
if ( ! empty( $state['threshold_reached'] ) ) {
// Reveal dependent UI or allow a next-step action.
}
Use get_watch_state() only when you explicitly need the raw row for a single post/video source.
AJAX Handlers
The main frontend/admin entry points are registered in src/Ajax/VideoWatchActions.php.
wp_ajax_brm_get_video_watch_state— Returns the raw watch-state row for the requested post plusrequired_percentafter logged-in, nonce, source, source-hash, and watch-token validation. This handler does not callget_effective_watch_state().wp_ajax_brm_sync_video_watch_state— Syncs client watch progress into the canonical watch tablewp_ajax_brm_record_video_timed_action— Since version 1.3.0, records timed-action fires only when the action’s run policy, reporting mode, or automation mode requires server-side storage/dispatch. It validates nonce, user/anonymous setting, source hash, and watch token for logged-in users before delegating toVideoActionLedgerService.wp_ajax_brm_get_video_text_track— Since version 1.3.0, returns parsed transcript cues for a saved text-track index after logged-in, nonce, source-hash, and watch-token validation. The request does not accept arbitrary track URLs.wp_ajax_brm_test_video_host_connection— Admin-only connection test for configured hosts
The read and sync payloads include watch_token. The sync payload also includes source_hash, duration_seconds, current_time_seconds, max_position_seconds, and bucket_indexes. The service rejects stale payloads if the source hash no longer matches the canonical post source.
AJAX watch-state reads and writes do not run BRM content access gates directly. If the BRM Video element rendered a playable video for a logged-in user, it issues a signed token for that user, post, and source hash. The AJAX handlers verify that token before reading or writing. Guests can watch videos that the page renders for them, but no guest watch state is saved and no wp_ajax_nopriv_* tracking endpoints are registered.
REST Endpoints
Hosted video browsing is exposed through the REST controller under /wp-json/bricksmembers/v1/video-picker/.
GET /wp-json/bricksmembers/v1/video-picker/{provider}— List videos for one providerGET /wp-json/bricksmembers/v1/video-picker/{provider}/groups— List groups/folders for providers that support them
The current picker supports mux, wistia, gumlet, bunny, youtube, vimeo, and cloudflare_stream. The routes are registered whenever the Video Tracking module is active. Requests require a logged-in user who can edit_posts plus a valid WordPress REST nonce. Successful responses still depend on the provider being enabled and connected. YouTube is considered connected when BRM has either a current access token or a refresh-token stack (refresh_token + client_id + client_secret). Vimeo requires a saved access token, and private libraries need the public and private scopes. Cloudflare Stream requires a Cloudflare account ID and API token with Stream read access.
Bunny picker selections store library_id/video_id. Manual Bunny sources can also be a Bunny embed URL or a bare video ID when the Bunny provider is explicit and the Library ID is saved. If Bunny is the only enabled and connected provider, VideoSourceService can resolve a bare Bunny ID without a saved provider hint. Provider detection otherwise does not treat arbitrary bare IDs as Bunny unless that hint exists.
Since version 1.3.0, Cloudflare Stream picker selections and manual sources normalize to provider key cloudflare_stream. Accepted manual values include a Stream UID, iframe.videodelivery.net, videodelivery.net, and customer-{code}.cloudflarestream.com Stream URLs.
Bricks Integration
The frontend element is brm-video (BRM Video in the builder). By default, it renders the canonical video source for the current post and handles tracked playback for logged-in users. Its source-context control can also resolve the active Bricks query loop item when a loop card should play and track that item’s own BRM video.
Frontend sync requests include the outer context post when the tracked video post differs from the rendered lesson/page. The brm:video:progress event carries persisted watched coverage after a successful sync response; live player position is exposed separately as brm:video:playback-progress. The BRM Progress Bar video mode chooses between those two frontend event streams through its Video Progress Value control.
- Display modes —
direct_embed,click_to_load,lightbox - Video source context —
current_postis the default and resolves the source from the current WordPress post.query_loopresolves throughBricksConditionContext::resolve_loop_post_id(). Both modes allow playback by default when the element is visible. - Playback Access — Optional element-level blocking. Modes are always allow, logged-in users only, selected BRM gates, and all BRM access gates. Selected gates can require assigned user levels chosen directly in the element, post required levels, enrollment, prerequisites, or drip. Blocked renders can show an overlay message or render nothing. A blocked render does not output playable source data.
- Playback speed — Since version 1.3.0, the element can show playback-rate controls. Native HTML5/HLS and supported provider APIs receive the rate at runtime. The selected speed is not persisted server-side and does not automatically carry to the next video or reload.
- Picture-in-Picture — Since version 1.3.0, native HTML5/HLS sources can show a PiP button when
document.pictureInPictureEnabledandvideo.requestPictureInPicture()are available. Iframe providers keep the iframe permission but do not show BRM’s native PiP control. - Keyboard shortcuts — Since version 1.3.0, focus-scoped transport shortcuts can toggle play/pause and seek backward/forward. Seek shortcuts route through the existing seek-policy guard so hard/soft skip restrictions remain enforceable.
- Off-screen behavior — Since version 1.3.0, the element can do nothing, pause when off-screen, enter a floating mini-player, or pause when the floating mini-player is closed.
- Chapter display — Since version 1.3.0, the element can leave chapters hidden or render native HTML5/HLS timeline markers from the canonical
chapterssource metadata. Timeline markers can use marker or segment styling, optional custom position/offset, configurable tooltip behavior, optional click-to-seek, and custom colors. Marker clicks use the existing seek guard. - Watermark — Since version 1.3.0, the element can render a server-resolved watermark overlay. Basic text, mode, and position controls stay visible only when watermarking is enabled. Advanced font size, weight, text/background color, opacity, padding, radius, and letter-case controls appear only when Customize watermark is enabled. Styling is passed through the existing element runtime config; no frontend CSS file is enqueued for the BRM Video element.
- Lazy loading and HLS — Since version 1.3.0, the element can choose eager, viewport, click, or play loading. Native HLS is preferred; otherwise the runtime loads the local
hls.jsasset for native video sources that need it. - Text tracks — Since version 1.3.0, saved text tracks render as native
<track>elements for native video sources, and transcript cues are available through the validated text-track AJAX endpoint. - Timed actions — Since version 1.3.0, up to five element-configured actions can fire by time, percent, threshold, or completion. Action types are partial refresh, full refresh, and redirect. Non-durable actions stay browser-only; durable once-per-user behavior, reporting, and automation dispatch require matching global Analytics tab settings.
- Threshold refresh control — Injected through
brm/elements/brm-video/controlsand backed byVideoRefreshService. Since version 1.3.0, threshold handling also supports redirects with optional completion confirmation, delay, and timeout. - Asset hook —
brm/elements/brm-video/enqueue_assets - Threshold event name —
brm:video:threshold-reached - Playback blocked event name —
brm:video:access-blocked
The threshold refresh controls support no-op, partial AJAX refresh, full page refresh with scroll/highlight restoration, or redirect. This is the hook point that lets the video threshold reveal quizzes and assessments, refresh progress bars, update wrapped BRM UI on the page, or navigate to a next step after optional completion confirmation.
Both source-context modes still write watch state through VideoWatchService for the resolved post ID. The element does not introduce a separate source or progress mutation path.
Dynamic Tags
Video watch tags are registered through VideoWatchDynamicTags. These tags use VideoWatchService::get_effective_watch_state(), not the raw AJAX state handler, so lesson-level aggregate contexts can resolve differently from wp_ajax_brm_get_video_watch_state. Current tags:
{brm_video_watch:percent}{brm_video_watch:required_percent}{brm_video_watch:remaining_percent}{brm_video_watch:threshold_reached}{brm_video_watch:completed}{brm_video_watch:last_position}{brm_video_watch:resume_available}
Separate from the watch-state tags, the core BRM dynamic tags also expose video metadata for the current post. That includes the authored {brm_video} value, the resolved provider embed via {brm_video:embed}, {brm_video:source}, {brm_video:title}, {brm_video:provider}, {brm_video:source_id}, {brm_video:image}, and {brm_video:duration}. {brm_video:source} remains backward compatible and returns the resolved provider key such as gumlet, vimeo, youtube, cloudflare_stream, or html5.
When a tracked video is also a Progress Tracking item, “last completed video” links are handled by the progress dynamic tag read path, not by the video-watch API: use {brm_progress:completed_item:last} in Bricks or CompletedItemTargetReadService inside plugin internals.
Provider Normalization
VideoProviderRegistry::normalize_from_raw() is the normalization boundary between editor input and runtime playback. The canonical source payload contains provider-specific normalized data such as provider key, source value, source ID, embed URL, thumbnail ID, duration, and privacy mode.
Provider implementations also declare whether they support resume and seek-guard behavior. That allows the runtime to adapt playback UX without treating all video sources as identical.
Bunny and Gumlet iframe tracking uses PlayerJS. Direct embeds are initialized after the PlayerJS library is available, and the runtime accepts both object payloads and JSON-string payloads for timeupdate events. Provider thumbnail mode may return a third-party thumbnail URL unless store_thumbnails_in_wp is enabled. Bunny thumbnails are resolved from Bunny API play data or stored WordPress media; the runtime does not construct guessed vz-{library_id}.b-cdn.net thumbnail URLs because those can be denied by Bunny CDN security or point at the wrong pull zone.
On pages with several BRM Video elements, the frontend queues initial brm_get_video_watch_state reads with low concurrency and deduplicates identical post/source reads. Playback sync writes remain immediate; only the page-load state reads are paced to avoid unnecessary admin-ajax.php bursts on query-loop templates.