diff --git a/src/app/exercise/exercise.page/components/answers-layout/answers-layout.component.scss b/src/app/exercise/exercise.page/components/answers-layout/answers-layout.component.scss
index 30539af5..19b76398 100644
--- a/src/app/exercise/exercise.page/components/answers-layout/answers-layout.component.scss
+++ b/src/app/exercise/exercise.page/components/answers-layout/answers-layout.component.scss
@@ -8,7 +8,6 @@
.answers-layout {
&__answers-rows-container,
&__answers-buttons-auto-layout-container {
- flex-grow: 1;
display: flex;
flex-direction: column;
gap: math.div($unit, 2);
diff --git a/src/app/exercise/exercise.page/exercise.page.html b/src/app/exercise/exercise.page/exercise.page.html
index a72a36b6..d3fb9fd1 100644
--- a/src/app/exercise/exercise.page/exercise.page.html
+++ b/src/app/exercise/exercise.page/exercise.page.html
@@ -71,6 +71,7 @@
}"
(answerSelected)="onAnswerSelected($event)"
>
+
@if (answerConfig.answer; as answer) {
diff --git a/src/app/exercise/exercise.page/exercise.page.scss b/src/app/exercise/exercise.page/exercise.page.scss
index 2dab8c1d..154200f0 100644
--- a/src/app/exercise/exercise.page/exercise.page.scss
+++ b/src/app/exercise/exercise.page/exercise.page.scss
@@ -31,3 +31,8 @@
ion-title {
padding: 0;
}
+
+.video-container {
+ flex-grow: 1;
+ text-align: center;
+}
diff --git a/src/app/exercise/exercise.page/state/exercise-state.service.ts b/src/app/exercise/exercise.page/state/exercise-state.service.ts
index 72aa6297..a6c365b1 100644
--- a/src/app/exercise/exercise.page/state/exercise-state.service.ts
+++ b/src/app/exercise/exercise.page/state/exercise-state.service.ts
@@ -1,6 +1,7 @@
-import { Injectable, OnDestroy, inject, signal } from '@angular/core';
+import { Injectable, OnDestroy, effect, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
+import { AlertController } from '@ionic/angular';
import * as _ from 'lodash';
import { defaults } from 'lodash';
import { filter, map } from 'rxjs/operators';
@@ -75,6 +76,7 @@ export class ExerciseStateService implements OnDestroy {
readonly answerList = this._answerList.asReadonly();
private _answerToLabelStringMap: Record =
this._getAnswerToLabelStringMap();
+ private readonly _alertController = inject(AlertController);
constructor() {
listenToChanges(this, '_currentQuestion')
@@ -90,6 +92,13 @@ export class ExerciseStateService implements OnDestroy {
this._dronePlayer.stopDrone();
}
});
+
+ effect(() => {
+ if (this._youtubePlayer.isPlaying() && this._autoPlayAlert) {
+ this._autoPlayAlert.dismiss();
+ this._autoPlayAlert = null;
+ }
+ });
}
get playerReady(): boolean {
@@ -462,7 +471,27 @@ export class ExerciseStateService implements OnDestroy {
private async _loadYoutubeQuestion(
question: Exercise.YouTubeQuestion,
): Promise {
- await this._youtubePlayer.loadVideoById(question.videoId);
+ await this._youtubePlayer.loadVideoById(question.videoId, () =>
+ this._handleAutoPlayBlocked(),
+ );
+ }
+
+ private _autoPlayAlert: HTMLIonAlertElement | null = null;
+ private async _handleAutoPlayBlocked() {
+ if (this._autoPlayAlert) {
+ return;
+ }
+ this._autoPlayAlert = await this._alertController.create({
+ message:
+ 'Autoplay for videos is not support on iOS. To work around this, please manually press the video to start',
+ subHeader: 'Press Play on the Video To Start',
+ buttons: ["I'll Press Play on the Video"],
+ cssClass: 'above-toaster',
+ });
+ await this._autoPlayAlert.present();
+ this._autoPlayAlert.onDidDismiss().then(() => {
+ this._autoPlayAlert = null;
+ });
}
private async _playYouTubeQuestion(
@@ -471,7 +500,7 @@ export class ExerciseStateService implements OnDestroy {
if (this._destroyed) {
return;
}
- if (this._youtubePlayer.isVideoLoading) {
+ if (this._youtubePlayer.isVideoLoading && !this._autoPlayAlert) {
this._showMessage('Video is loading...');
this._youtubePlayer.onCurrentVideoLoaded.then(() => {
this._hideMessage();
@@ -495,6 +524,7 @@ export class ExerciseStateService implements OnDestroy {
},
},
],
+ () => this._handleAutoPlayBlocked(),
);
this._adaptiveExercise.questionStartedPlaying();
await this._youtubePlayer.onStop();
diff --git a/src/app/services/you-tube-player.service.ts b/src/app/services/you-tube-player.service.ts
index 4716ee9f..dcfa64cd 100644
--- a/src/app/services/you-tube-player.service.ts
+++ b/src/app/services/you-tube-player.service.ts
@@ -1,10 +1,9 @@
-import { Injectable } from '@angular/core';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { computed, Injectable } from '@angular/core';
+import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import * as PriorityQueue from 'js-priority-queue';
import { BehaviorSubject, interval, NEVER } from 'rxjs';
import { filter, switchMap, take } from 'rxjs/operators';
import PlayerFactory from 'youtube-player';
-import { YouTubePlayer } from 'youtube-player/dist/types';
export interface YouTubeCallbackDescriptor {
seconds: number;
@@ -17,11 +16,15 @@ const TIME_STAMP_POLLING: number = 200;
providedIn: 'root',
})
export class YouTubePlayerService {
+ private _elm = this._getHostElement();
private _isVideoLoading: boolean = false;
private _currentlyLoadedVideoId: string | null = null;
private _onCurrentVideoLoaded: Promise = Promise.resolve();
private _isPlaying$ = new BehaviorSubject(false);
- private _youTubePlayer: YouTubePlayer = this._getYouTubePlayer();
+ readonly isPlaying = toSignal(this._isPlaying$.asObservable(), {
+ initialValue: false,
+ });
+ private _youTubePlayer = this._getYouTubePlayer();
private _callBackQueue = new PriorityQueue({
comparator: (
a: YouTubeCallbackDescriptor,
@@ -39,10 +42,6 @@ export class YouTubePlayerService {
return this._onCurrentVideoLoaded;
}
- get isPlaying() {
- return this._isPlaying$.value;
- }
-
constructor() {
this._startTimeListener();
}
@@ -50,28 +49,36 @@ export class YouTubePlayerService {
/**
* This method will not load the video if it's already loaded
* */
- async loadVideoById(videoId: string): Promise {
+ async loadVideoById(
+ videoId: string,
+ onAutoplayBlocked: () => void,
+ ): Promise {
if (this._currentlyLoadedVideoId !== videoId) {
this._currentlyLoadedVideoId = videoId;
this._isVideoLoading = true;
- this._onCurrentVideoLoaded = this._youTubePlayer
+ this._onCurrentVideoLoaded = this._youTubePlayer()
.loadVideoById(videoId)
.then(() => {
return new Promise((resolve) => {
- const listener = this._youTubePlayer.on(
+ const listener = this._youTubePlayer().on(
'stateChange',
({ data }) => {
+ console.log('stateChange', data); // todo
+
if (data === 1) {
// @ts-ignore
- this._youTubePlayer.off(listener);
+ this._youTubePlayer().off(listener);
resolve();
}
+ if (data === -1) {
+ onAutoplayBlocked();
+ }
},
);
});
})
.then(() => {
- this._youTubePlayer.pauseVideo(); // we don't always want it to start playing immediately
+ this._youTubePlayer().pauseVideo(); // we don't always want it to start playing immediately
})
.then(() => {
this._isVideoLoading = false;
@@ -91,14 +98,15 @@ export class YouTubePlayerService {
videoId: string,
time: number,
callbacks: YouTubeCallbackDescriptor[] = [],
+ onAutoplayBlocked: () => void,
): Promise {
console.log('play', videoId, time);
if (videoId !== this._currentlyLoadedVideoId) {
- await this.loadVideoById(videoId);
+ await this.loadVideoById(videoId, onAutoplayBlocked);
}
await this._onCurrentVideoLoaded; // it's possible loadVideoById was invoked by another function but video is not loaded yet
- await this._youTubePlayer.seekTo(time, true);
- await this._youTubePlayer.playVideo();
+ await this._youTubePlayer().seekTo(time, true);
+ await this._youTubePlayer().playVideo();
this._isPlaying$.next(true);
callbacks.forEach((callback) => {
this._callBackQueue.queue(callback);
@@ -107,7 +115,7 @@ export class YouTubePlayerService {
async stop(): Promise {
if (this._isPlaying$.value) {
- await this._youTubePlayer.pauseVideo(); // used instead of stopVideo to avoid resetting of the play position
+ await this._youTubePlayer().pauseVideo(); // used instead of stopVideo to avoid resetting of the play position
this._callBackQueue.clear();
this._isPlaying$.next(false);
}
@@ -126,15 +134,8 @@ export class YouTubePlayerService {
}
}
- private _getYouTubePlayer(): YouTubePlayer {
- const elm = document.createElement('div');
- // Expose the following code for debugging purposes
- // elm.style['position'] = 'absolute';
- // elm.style['top'] = '0';
- // elm.style['width'] = '100px';
- // elm.style['height'] = '100px';
- document.body.appendChild(elm);
- return PlayerFactory(elm);
+ private _getYouTubePlayer() {
+ return computed(() => PlayerFactory(this._elm()));
}
private _startTimeListener(): void {
@@ -154,11 +155,33 @@ export class YouTubePlayerService {
return;
}
const nextCallback = this._callBackQueue.peek();
- const currentTime = await this._youTubePlayer.getCurrentTime();
+ const currentTime = await this._youTubePlayer().getCurrentTime();
if (currentTime > nextCallback.seconds - TIME_STAMP_POLLING / 2000) {
this._callBackQueue.dequeue();
nextCallback.callback();
}
});
}
+
+ private _getHostElement() {
+ return computed(() => {
+ const elm = document.createElement('div');
+ let host =
+ document.querySelector('.video-container') ??
+ document.body;
+
+ // if (!elm) {
+ // console.warn('Video container not found');
+
+ // elm = document.createElement('div');
+ // }
+ // Expose the following code for debugging purposes
+ // elm.style['position'] = 'absolute';
+ // elm.style['top'] = '0';
+ elm.style['width'] = '70%';
+ elm.style['height'] = '150px';
+ host.appendChild(elm);
+ return elm;
+ });
+ }
}
diff --git a/src/style/overrides/index.scss b/src/style/overrides/index.scss
index 27aead97..589170e5 100644
--- a/src/style/overrides/index.scss
+++ b/src/style/overrides/index.scss
@@ -11,3 +11,7 @@ ion-toggle,
ion-select {
flex-grow: 0;
}
+
+.above-toaster {
+ z-index: 60003 !important;
+}