ソースを参照

VideoSource: improve the handling of autoplay quirks

customisations
alemart 1年前
コミット
f9a58d5962
1個のファイルの変更112行の追加53行の削除
  1. 112
    53
      src/sources/video-source.ts

+ 112
- 53
src/sources/video-source.ts ファイルの表示

@@ -24,9 +24,15 @@ import Speedy from 'speedy-vision';
24 24
 import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
25 25
 import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
26 26
 import { Utils, Nullable } from '../utils/utils';
27
-import { IllegalOperationError, NotSupportedError } from '../utils/errors';
27
+import { IllegalOperationError, NotSupportedError, TimeoutError } from '../utils/errors';
28 28
 import { Source } from './source';
29 29
 
30
+/** A message to be displayed if a video can't autoplay and user interaction is required */
31
+const ALERT_MESSAGE = 'Tap on the screen to start';
32
+
33
+/** Whether or not we have displayed the ALERT_MESSAGE */
34
+let displayedAlertMessage = false;
35
+
30 36
 
31 37
 /**
32 38
  * HTMLVideoElement-based source of data
@@ -94,11 +100,10 @@ export class VideoSource implements Source
94 100
      */
95 101
     _init(): SpeedyPromise<void>
96 102
     {
97
-        this._handleBrowserQuirks(this._video);
98
-
99 103
         return Speedy.load(this._video).then(media => {
100 104
             Utils.log(`Source of data is a ${media.width}x${media.height} ${this._type}`);
101 105
             this._media = media;
106
+            return this._handleBrowserQuirks(this._video).then(() => void(0));
102 107
         });
103 108
     }
104 109
 
@@ -118,10 +123,11 @@ export class VideoSource implements Source
118 123
 
119 124
     /**
120 125
      * Handle browser-specific quirks for <video> elements
121
-     * @param video
126
+     * @param video a video element
127
+     * @returns a promise that resolves to the input video
122 128
      * @internal
123 129
      */
124
-    _handleBrowserQuirks(video: HTMLVideoElement): void
130
+    _handleBrowserQuirks(video: HTMLVideoElement): SpeedyPromise<HTMLVideoElement>
125 131
     {
126 132
         // WebKit <video> policies for iOS:
127 133
         // https://webkit.org/blog/6784/new-video-policies-for-ios/
@@ -130,80 +136,133 @@ export class VideoSource implements Source
130 136
         video.setAttribute('playsinline', '');
131 137
 
132 138
         // handle autoplay
133
-        if(video.autoplay)
134
-            this._handleAutoPlay(video);
135
-
136
-        // Handle WebKit quirks
137
-        // note: navigator.vendor is deprecated. Alternatively, test GL_RENDERER == "Apple GPU"
138
-        if(Utils.isIOS() || /Apple/.test(navigator.vendor)) {
139
-
140
-            // on Epiphany, a hidden <video> shows up as a black screen when copied to a canvas
141
-            if(video.hidden) {
142
-                video.hidden = false;
143
-                video.style.setProperty('opacity', '0');
144
-                video.style.setProperty('position', 'absolute');
139
+        return this._handleAutoPlay(video).then(video => {
140
+
141
+            // Handle WebKit quirks
142
+            // note: navigator.vendor is deprecated. Alternatively, test GL_RENDERER == "Apple GPU"
143
+            if(Utils.isIOS() || /Apple/.test(navigator.vendor)) {
144
+
145
+                // on Epiphany, a hidden <video> shows up as a black screen when copied to a canvas
146
+                if(video.hidden) {
147
+                    video.hidden = false;
148
+                    video.style.setProperty('opacity', '0');
149
+                    video.style.setProperty('position', 'absolute');
150
+                }
151
+
145 152
             }
146 153
 
147
-        }
154
+            // done
155
+            return video;
156
+
157
+        });
148 158
     }
149 159
 
150 160
     /**
151 161
      * Handle browser-specific quirks for videos marked with autoplay
152 162
      * @param video a <video> marked with autoplay
163
+     * @returns a promise that resolves to the input video
153 164
      * @internal
154 165
      */
155
-    _handleAutoPlay(video: HTMLVideoElement): void
166
+    _handleAutoPlay(video: HTMLVideoElement): SpeedyPromise<HTMLVideoElement>
156 167
     {
157 168
         // Autoplay guide: https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
158 169
         // Chrome policy: https://developer.chrome.com/blog/autoplay/
159 170
         // WebKit policy: https://webkit.org/blog/7734/auto-play-policy-changes-for-macos/
160
-        Utils.assert(video.autoplay);
171
+
172
+        // nothing to do?
173
+        if(!video.autoplay)
174
+            return Speedy.Promise.resolve(video);
161 175
 
162 176
         // videos marked with autoplay should be muted
163 177
         video.muted = true;
164 178
 
165 179
         // the browser may not honor the autoplay attribute if the video is not
166 180
         // visible on-screen. So, let's try to play the video in any case.
167
-        video.addEventListener('canplay', () => {
181
+        return this._waitUntilPlayable(video).then(video => {
182
+
183
+            // try to play the video
168 184
             const promise = video.play();
169 185
 
170 186
             // handle older browsers
171 187
             if(promise === undefined)
172
-                return;
173
-
174
-            // can't play the video
175
-            promise.catch((error: DOMException) => {
176
-                Utils.error(`Can't autoplay video!`, error, video);
177
-
178
-                // autoplay is blocked for some reason
179
-                if(error.name == 'NotAllowedError') {
180
-                    Utils.warning('Tip: allow manual playback');
181
-
182
-                    if(Utils.isIOS())
183
-                        Utils.warning('Is low power mode on?');
184
-
185
-                    // User interaction is required to play the video. We can
186
-                    // solve this here (easy and convenient to do) or at the
187
-                    // application layer (for a better user experience). If the
188
-                    // latter is preferred, just disable autoplay and play the
189
-                    // video programatically.
190
-                    if(video.hidden || !video.controls || video.parentNode === null) {
191
-                        // this is added for convenience
192
-                        document.body.addEventListener('pointerdown', () => video.play());
193
-                        alert('Tap on the screen to start');
188
+                return video;
189
+
190
+            // resolve if successful
191
+            return new Speedy.Promise<HTMLVideoElement>((resolve, reject) => {
192
+                promise.then(() => resolve(video), error => {
193
+                    // can't play the video
194
+                    Utils.error(`Can't autoplay video!`, error, video);
195
+
196
+                    // autoplay is blocked for some reason
197
+                    if(error.name == 'NotAllowedError') {
198
+                        Utils.warning('Tip: allow manual playback');
199
+
200
+                        if(Utils.isIOS())
201
+                            Utils.warning('Is low power mode on?');
202
+
203
+                        // User interaction is required to play the video. We can
204
+                        // solve this here (easy and convenient to do) or at the
205
+                        // application layer (for a better user experience). If the
206
+                        // latter is preferred, just disable autoplay and play the
207
+                        // video programatically.
208
+                        if(video.hidden || !video.controls || video.parentNode === null) {
209
+
210
+                            // this is added for convenience
211
+                            document.body.addEventListener('pointerdown', () => video.play());
212
+
213
+                            // display the interactive message only once
214
+                            if(!displayedAlertMessage) {
215
+                                alert(ALERT_MESSAGE);
216
+                                displayedAlertMessage = true;
217
+                            }
218
+
219
+                        }
220
+                        /*else {
221
+                            // play the video after the first interaction with the page
222
+                            const polling = setInterval(() => {
223
+                                video.play().then(() => clearInterval(polling));
224
+                            }, 500);
225
+                        }*/
226
+                    }
227
+
228
+                    // unsupported media source
229
+                    else if(error.name == 'NotSupportedError') {
230
+                        reject(new NotSupportedError('Unsupported video format', error));
231
+                        return;
194 232
                     }
195
-                    /*else {
196
-                        // play the video after the first interaction with the page
197
-                        const polling = setInterval(() => {
198
-                            video.play().then(() => clearInterval(polling));
199
-                        }, 500);
200
-                    }*/
201
-                }
202 233
 
203
-                // unsupported media source
204
-                else if(error.name == 'NotSupportedError')
205
-                    throw new NotSupportedError('Unsupported video format', error);
234
+                    // done
235
+                    resolve(video);
236
+                });
206 237
             });
207 238
         });
208 239
     }
240
+
241
+    /**
242
+     * Wait for the input video to be playable
243
+     * @param video
244
+     * @returns a promise that resolves to the input video when it can be played through to the end
245
+     * @internal
246
+     */
247
+    _waitUntilPlayable(video: HTMLVideoElement): SpeedyPromise<HTMLVideoElement>
248
+    {
249
+        const TIMEOUT = 15000, INTERVAL = 500;
250
+
251
+        if(video.readyState >= 4)
252
+            return Speedy.Promise.resolve(video);
253
+
254
+        return new Speedy.Promise<HTMLVideoElement>((resolve, reject) => {
255
+            let ms = 0, t = setInterval(() => {
256
+
257
+                if(video.readyState >= 4) { // canplaythrough
258
+                    clearInterval(t);
259
+                    resolve(video);
260
+                }
261
+                else if((ms += INTERVAL) > TIMEOUT) {
262
+                    reject(new TimeoutError('The video took too long to load'));
263
+                }
264
+
265
+            }, INTERVAL);
266
+        });
267
+    }
209 268
 }

読み込み中…
キャンセル
保存