Я разрабатываю сложное угловое веб-приложение с глубоким деревом компонентов.Основная идея этого приложения состоит в том, чтобы показывать различные задачи (какую-то викторину) через видеопоток.Некоторые задачи появляются, когда видео продолжает воспроизводиться, в то время как другие задачи приостанавливают воспроизведение видео, пока задача не будет завершена.Но каждая задача должна появляться точно в указанное время.
Проблема заключается в том, что в некоторых обстоятельствах (я не могу понять, от чего именно это поведение зависит) некоторые задачи появляются со значительным отставанием (5-10 секунд).Такое поведение не является регулярным, поэтому трудно поймать и отладить, в чем причина.Похоже, такое поведение чаще встречается, когда страница «холодный старт», в то время как любая попытка воспроизвести ее на той же странице не приносит успеха.
Вот причины, которые были рассмотрены и отброшены:
Обнаружение изменений.Я думал, что Angular не обнаруживает появление новой задачи и не запускает код рендеринга.Но это не так, потому что:
- есть явный вызов ChangeDetector.detectChanges () для задачи;
- код инициализации задачи также воспроизводит звук (через WebAudioApi), и я всегда слышу звук в указанное время без задержки, но визуально элементы отображаются с задержкой.
Браузер сильно загружен какой-то работой и не может показать задачу вовремя.Я полагаю, что это не так, потому что я потратил несколько часов на запись и анализ профиля производительности Chrome и не нашел там большой нагрузки.И наоборот, в проблемные моменты есть подозрительное безделье.Вот пример:

Проблемы с движком браузера.Я отказался от этой опции, потому что видел это поведение в Chrome, Safari и Firefox (в Windows, MacOS, iOS)
Вот фрагменты кода, связанные с появлением задачи:
quest.component.ts
@Component({
selector: 'quest',
templateUrl: 'quest.component.html',
providers: [
HintsManagerService,
UserHintService,
HearNoteSyncService
]
})
export class QuestComponent implements OnInit, OnDestroy, AfterViewInit {
private questPaused = false;
private subscriptions: Subscription[] = new Array<Subscription>();
private currentTime: number;
private maxReward = 0;
private currentReward = 0;
private videoProportion: [number, number];
private wrappedTasks: TaskWrapper[];
private lastActivatedTask = -1;
private questPass: QuestPass = new QuestPass();
private forciblyClosed = false;
private isFullScreenActivated = false;
private anticipationTime: number = 0.05;
private rewindThresholdTime: number = 1;
private anyTaskTutorial: boolean = false;
@Input() settings: QuestSettings;
@Input() quest: Quest;
@Input() config: QuestConfig;
@Input() scoreUnit = 'coin';
@Input() battleId: number;
@Input() videoChallengeRoundId: number;
@Output() questFinished = new EventEmitter<{ score: number, forciblyClosed: boolean, questPass: QuestPass }>();
@ViewChild('player') videoPlayer: PlayerComponent;
@ViewChild('topPlayer') topPlayer: ElementRef;
constructor(
private store: Store<AppState>,
private playerTimerService: PlayerTimerService,
private changeDetector: ChangeDetectorRef,
private hintManagerService: HintsManagerService,
private tutorialService: TutorialService,
private soundService: SoundService) {
}
ngOnInit() {
this.subscriptions.push(this.store.select(state => state.gameControl.questEnd)
.subscribe(questEnd => { if (questEnd) { this.stopQuest(); } }));
this.subscriptions.push(this.store.select(state => state.gameControl.pause)
.subscribe(pause => { this.questPaused = pause; }));
this.wrappedTasks = new Array<TaskWrapper>();
this.quest.tasks.forEach(value => {
this.wrappedTasks.push({
startTime: value.startTime,
active: false,
activated: false,
task: value,
showTaskTutorial: this.tutorialService.shouldShowTaskTutorial(this.quest, value)
} as TaskWrapper);
this.maxReward += value.getTotalReward();
});
this.wrappedTasks.sort((a: TaskWrapper, b: TaskWrapper) => a.startTime - b.startTime);
this.anyTaskTutorial = this.wrappedTasks.some(wt => wt.showTaskTutorial);
this.hintManagerService.initialize(this.wrappedTasks.map(wt => wt.task), this.quest, this.settings, this.battleId, this.videoChallengeRoundId);
}
ngAfterViewInit() {
this.subscriptions.push(this.videoPlayer.onUserRequestPlayPause
.subscribe(value => this.tryPlayPause(value)));
this.subscriptions.push(this.playerTimerService.timerUpdated
.subscribe(value => {
if (!this.questPaused) {
this.currentTime = value;
this.checkActiveTasks();
}
}));
}
answerClick(task: QuestTask, result: TaskPassResult) {
if (result.isCorrect) {
this.currentReward += task.reward;
}
let currentScore = 0;
switch (this.scoreUnit) {
case 'coin':
currentScore = Math.round(this.quest.coinReward * this.currentReward / this.maxReward);
break;
case 'percent':
currentScore = Math.round(100 * this.currentReward / this.maxReward);
break;
}
this.videoPlayer.updateScore(currentScore);
if (result.isCorrect) {
this.soundService.play('right-answer');
} else {
this.soundService.play('wrong-answer');
}
}
checkActiveTasks() {
this.hintManagerService.checkTasksIntersection(this.currentTime);
for (let i = this.lastActivatedTask + 1; i < this.wrappedTasks.length; ++i) {
if (this.wrappedTasks[i].startTime - this.anticipationTime > this.currentTime) {
break;
} else if (this.wrappedTasks[i].startTime - this.anticipationTime <= this.currentTime && !this.wrappedTasks[i].activated && !this.wrappedTasks[i].active) {
this.wrappedTasks[i].tutorials = this.settings.tutorialEnabled
? this.tutorialService.getTutorialsForTask(this.anyTaskTutorial, this.wrappedTasks[i].showTaskTutorial, this.wrappedTasks[i].task)
: [];
this.wrappedTasks[i].active = true;
this.wrappedTasks[i].activated = true;
this.lastActivatedTask = i;
if (this.wrappedTasks[i].task.pauseRequired) {
this.store.dispatch({ type: VIDEO_PAUSE, payload: true });
this.videoPlayer.setPlaybackTime(this.wrappedTasks[i].startTime);
}
this.changeDetector.detectChanges();
// if we skipped too much time, then do rewind and stop cycle to prevent simultaneous task activation
if (this.currentTime - this.wrappedTasks[i].startTime > this.rewindThresholdTime) {
// if not yet rewinded
if (!this.wrappedTasks[i].task.pauseRequired) {
this.videoPlayer.setPlaybackTime(this.wrappedTasks[i].startTime);
}
break;
}
}
}
}
deactivateTask(wrappedTask: TaskWrapper) {
wrappedTask.active = false;
}
closeQuest() {
this.forciblyClosed = true;
this.videoPlayer.close();
}
stopQuest() {
this.questFinished.emit({
score : this.currentReward / this.maxReward,
forciblyClosed: this.forciblyClosed,
questPass: this.questPass
});
}
tryPlayPause(paused: boolean): void {
const canPause = this.wrappedTasks.filter(wt => wt.active && wt.task.type !== TaskType.hear && wt.task.type !== TaskType.note).length === 0;
if (canPause) {
this.questPaused = !paused;
this.store.dispatch({ type: PAUSE });
}
}
ngOnDestroy() {
this.subscriptions.forEach((subscription: Subscription) => {
subscription.unsubscribe();
});
this.hintManagerService.destroyService();
this.store.dispatch({ type: RESET });
}
}
quest.component.html
<ng-container *ngIf="quest">
<div class="top__player" #topPlayer [ngClass]="topPlayer.offsetHeight | questSize : applySizePipe">
<player class="player" #player
[quest]="quest"
[startTime]="quest?.startTime"
[duration]="quest?.duration"
[scoreUnit]="scoreUnit"
[hintsEnabled]="settings?.hintsEnabled"
(onQuestInterrupted)="stopQuest()">
<ng-container *ngFor="let wrappedTask of wrappedTasks">
<ng-container [ngSwitch]="wrappedTask.task.type" *ngIf="wrappedTask.active">
<quiz-task *ngSwitchCase="'quiz'"
[tutorialTypes]="wrappedTask.tutorials"
[task]="wrappedTask.task"
(onAnswer)="answerClick(wrappedTask.task, $event)"
(onDeactivate)="deactivateTask(wrappedTask)">
</quiz-task>
<hidden-area-task *ngSwitchCase="'hidden-area'"
[tutorialTypes]="wrappedTask.tutorials"
[task]="wrappedTask.task"
(onAnswer)="answerClick(wrappedTask.task, $event)"
(onDeactivate)="deactivateTask(wrappedTask)">
</hidden-area-task>
<whats-next-task *ngSwitchCase="'whats-next'"
[tutorialTypes]="wrappedTask.tutorials"
[task]="wrappedTask.task"
(onAnswer)="answerClick(wrappedTask.task, $event)"
(onDeactivate)="deactivateTask(wrappedTask)">
</whats-next-task>
<hear-note-task *ngSwitchCase="'hear'"
[tutorialTypes]="wrappedTask.tutorials"
[task]="wrappedTask.task"
[taskType]="'hear_box'"
[reactionTime]="config.hearReactionTime"
(onAnswer)="answerClick(wrappedTask.task, $event)"
(onDeactivate)="deactivateTask(wrappedTask)">
</hear-note-task>
<hear-note-task *ngSwitchCase="'note'"
[tutorialTypes]="wrappedTask.tutorials"
[task]="wrappedTask.task"
[taskType]="'note_box'"
[reactionTime]="config.noteReactionTime"
(onAnswer)="answerClick(wrappedTask.task, $event)"
(onDeactivate)="deactivateTask(wrappedTask)">
</hear-note-task>
<div *ngSwitchDefault>Unknown type of task</div>
</ng-container>
</ng-container>
</player>
</div>
</ng-container>