diff --git a/docs/components/topicComponent.md b/docs/components/topicComponent.md index b11931ed1..a23908b9a 100644 --- a/docs/components/topicComponent.md +++ b/docs/components/topicComponent.md @@ -229,7 +229,7 @@ private _initVideoPlayer() { ```typescript this.plyrInitialized = false; this.plyrNeedsInit = false; // Added: was missing before - ```loads Plyr overlay + ``` - [x] Subsequent visits to SAME task loads Plyr overlay correctly - [x] Switching between different video tasks works - [x] Native video elements (non-YouTube) still work @@ -238,17 +238,6 @@ private _initVideoPlayer() { - [x] Video controls are fully interactive on all visits - [x] Cleanup properly destroys old Plyr instances -**Loading Indicators (Added January 16, 2026):** -- [ ] Skeleton loader appears immediately when navigating to topic with video -- [ ] Skeleton has correct 16:9 aspect ratio (400px mobile, 450px desktop) -- [ ] Skeleton disappears when YouTube/Vimeo embed finishes Plyr initialization -- [ ] Skeleton disappears when native video triggers `canplay` event -- [ ] Fade-in animation plays smoothly after skeleton disappears (300ms) -- [ ] Skeleton clears on video error (test with invalid video URL) -- [ ] No flash of unstyled content between skeleton and video -- [ ] Skeleton has proper ARIA attributes (`role="status"`, `aria-label`) -- [ ] Loading state doesn't get stuck (test rapid navigation between topics) - **Poster Behavior (Updated January 16, 2026):** - [x] No poster overlay blocks video interaction (Plyr `poster: false` config) - [x] No console errors about poster removal diff --git a/projects/v3/src/app/components/topic/topic.component.html b/projects/v3/src/app/components/topic/topic.component.html index 1eac64235..c5c4497c0 100644 --- a/projects/v3/src/app/components/topic/topic.component.html +++ b/projects/v3/src/app/components/topic/topic.component.html @@ -6,13 +6,14 @@ #topicVideo *ngIf="!iframeHtml && videoSrc" [attr.id]="'topic-video-' + topic.id" - class="topic-video" + class="topic-video video-embed" [ngClass]="{'desktop-view': !isMobile}" width="100%" controls controlsList="nodownload" - preload="auto" + preload="metadata" playsinline + disabled [src]="videoSrc" (canplay)="onVideoCanPlay($event)" (error)="handleVideoError($event)" diff --git a/projects/v3/src/app/components/topic/topic.component.ts b/projects/v3/src/app/components/topic/topic.component.ts index 3a87070c4..1311f72ca 100644 --- a/projects/v3/src/app/components/topic/topic.component.ts +++ b/projects/v3/src/app/components/topic/topic.component.ts @@ -9,7 +9,7 @@ import { SafeHtml, DomSanitizer } from '@angular/platform-browser'; import { FilestackService } from '@v3/app/services/filestack.service'; import { NotificationsService } from '@v3/app/services/notifications.service'; import { BehaviorSubject, exhaustMap, filter, finalize, Subject, Subscription, takeUntil } from 'rxjs'; -import { Activity, Task } from '@v3/app/services/activity.service'; +import { Task } from '@v3/app/services/activity.service'; import { ComponentCleanupService } from '@v3/app/services/component-cleanup.service'; @Component({ @@ -40,6 +40,7 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe private plyrNeedsInit = false; private plyrInitialized = false; private destroy$ = new Subject(); + private plyrInstances = new WeakMap(); constructor( private embedService: EmbedVideoService, @@ -130,10 +131,13 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe if (this.videoNeedsInit && this.topicVideo?.nativeElement) { this.videoNeedsInit = false; + // Capture the videoSrc value to avoid race conditions + const capturedSrc = this.videoSrc; requestAnimationFrame(() => { const videoEl = this.topicVideo?.nativeElement; - if (videoEl && this.videoSrc) { - videoEl.src = this.videoSrc; + // Validate that the video element still exists and the src hasn't changed + if (videoEl && capturedSrc && videoEl.src !== capturedSrc) { + videoEl.src = capturedSrc; videoEl.load(); } }); @@ -176,15 +180,16 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe this._pauseResetAndReplaceMediaElements('video'); // destroy and remove all plyr instances - const plyrElements = this.document.querySelectorAll('.plyr'); + const plyrElements = this.document.querySelectorAll('.plyr__video-embed'); this.utils.each(plyrElements, (plyrEl: HTMLElement) => { - if ((plyrEl as any).plyr) { + const plyrInstance = this.plyrInstances.get(plyrEl); + if (plyrInstance) { try { - (plyrEl as any).plyr.destroy(); + plyrInstance.destroy(); } catch (e) { console.warn('error destroying plyr instance:', e); } - (plyrEl as any).plyr = null; + this.plyrInstances.delete(plyrEl); } }); @@ -261,10 +266,9 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe } }); - // store plyr instance reference for cleanup - (embedVideo as any).plyr = plyrInstance; + // store plyr instance reference for cleanup using WeakMap + this.plyrInstances.set(embedVideo, plyrInstance); - embedVideo.classList.add('topic-custome-player'); if (!this.utils.isMobile()) { embedVideo.classList.add('desktop-view'); }