ソースを参照

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
 import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
24
 import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
25
 import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
25
 import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
26
 import { Utils, Nullable } from '../utils/utils';
26
 import { Utils, Nullable } from '../utils/utils';
27
-import { IllegalOperationError, NotSupportedError } from '../utils/errors';
27
+import { IllegalOperationError, NotSupportedError, TimeoutError } from '../utils/errors';
28
 import { Source } from './source';
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
  * HTMLVideoElement-based source of data
38
  * HTMLVideoElement-based source of data
94
      */
100
      */
95
     _init(): SpeedyPromise<void>
101
     _init(): SpeedyPromise<void>
96
     {
102
     {
97
-        this._handleBrowserQuirks(this._video);
98
-
99
         return Speedy.load(this._video).then(media => {
103
         return Speedy.load(this._video).then(media => {
100
             Utils.log(`Source of data is a ${media.width}x${media.height} ${this._type}`);
104
             Utils.log(`Source of data is a ${media.width}x${media.height} ${this._type}`);
101
             this._media = media;
105
             this._media = media;
106
+            return this._handleBrowserQuirks(this._video).then(() => void(0));
102
         });
107
         });
103
     }
108
     }
104
 
109
 
118
 
123
 
119
     /**
124
     /**
120
      * Handle browser-specific quirks for <video> elements
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
      * @internal
128
      * @internal
123
      */
129
      */
124
-    _handleBrowserQuirks(video: HTMLVideoElement): void
130
+    _handleBrowserQuirks(video: HTMLVideoElement): SpeedyPromise<HTMLVideoElement>
125
     {
131
     {
126
         // WebKit <video> policies for iOS:
132
         // WebKit <video> policies for iOS:
127
         // https://webkit.org/blog/6784/new-video-policies-for-ios/
133
         // https://webkit.org/blog/6784/new-video-policies-for-ios/
130
         video.setAttribute('playsinline', '');
136
         video.setAttribute('playsinline', '');
131
 
137
 
132
         // handle autoplay
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
      * Handle browser-specific quirks for videos marked with autoplay
161
      * Handle browser-specific quirks for videos marked with autoplay
152
      * @param video a <video> marked with autoplay
162
      * @param video a <video> marked with autoplay
163
+     * @returns a promise that resolves to the input video
153
      * @internal
164
      * @internal
154
      */
165
      */
155
-    _handleAutoPlay(video: HTMLVideoElement): void
166
+    _handleAutoPlay(video: HTMLVideoElement): SpeedyPromise<HTMLVideoElement>
156
     {
167
     {
157
         // Autoplay guide: https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
168
         // Autoplay guide: https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
158
         // Chrome policy: https://developer.chrome.com/blog/autoplay/
169
         // Chrome policy: https://developer.chrome.com/blog/autoplay/
159
         // WebKit policy: https://webkit.org/blog/7734/auto-play-policy-changes-for-macos/
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
         // videos marked with autoplay should be muted
176
         // videos marked with autoplay should be muted
163
         video.muted = true;
177
         video.muted = true;
164
 
178
 
165
         // the browser may not honor the autoplay attribute if the video is not
179
         // the browser may not honor the autoplay attribute if the video is not
166
         // visible on-screen. So, let's try to play the video in any case.
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
             const promise = video.play();
184
             const promise = video.play();
169
 
185
 
170
             // handle older browsers
186
             // handle older browsers
171
             if(promise === undefined)
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
 }

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