1
1
import window from 'global/window' ;
2
+ import document from 'global/document' ;
2
3
import mergeOptions from '../utils/merge-options' ;
3
4
4
- const setupSourceset = function ( tech ) {
5
+ /**
6
+ * This function is used to fire a sourceset when there is something
7
+ * similar to `mediaEl.load()` being called. It will try to find the source via
8
+ * the `src` attribute and then the `<source>` elements. It will then fire `sourceset`
9
+ * with the source that was found or empty string if we cannot know. If it cannot
10
+ * find a source then `sourceset` will not be fired.
11
+ *
12
+ * @param {Html5 } tech
13
+ * The tech object that sourceset was setup on
14
+ *
15
+ * @return {boolean }
16
+ * returns false if the sourceset was not fired and true otherwise.
17
+ */
18
+ const sourcesetLoad = ( tech ) => {
19
+ const el = tech . el ( ) ;
5
20
6
- if ( ! tech . featuresSourceset ) {
21
+ // if `el.src` is set, that source will be loaded.
22
+ if ( el . src ) {
23
+ tech . triggerSourceset ( el . src ) ;
24
+ return true ;
25
+ }
26
+
27
+ /**
28
+ * Since there isn't a src property on the media element, source elements will be used for
29
+ * implementing the source selection algorithm. This happens asynchronously and
30
+ * for most cases were there is more than one source we cannot tell what source will
31
+ * be loaded, without re-implementing the source selection algorithm. At this time we are not
32
+ * going to do that. There are three special cases that we do handle here though:
33
+ *
34
+ * 1. If there are no sources, do not fire `sourceset`.
35
+ * 2. If there is only one `<source>` with a `src` property/attribute that is our `src`
36
+ * 3. If there is more than one `<source>` but all of them have the same `src` url.
37
+ * That will be our src.
38
+ */
39
+ const sources = tech . $$ ( 'source' ) ;
40
+ const srcUrls = [ ] ;
41
+ let src = '' ;
42
+
43
+ // if there are no sources, do not fire sourceset
44
+ if ( ! sources . length ) {
45
+ return false ;
46
+ }
47
+
48
+ // only count valid/non-duplicate source elements
49
+ for ( let i = 0 ; i < sources . length ; i ++ ) {
50
+ const url = sources [ i ] . src ;
51
+
52
+ if ( url && srcUrls . indexOf ( url ) === - 1 ) {
53
+ srcUrls . push ( url ) ;
54
+ }
55
+ }
56
+
57
+ // there were no valid sources
58
+ if ( ! srcUrls . length ) {
7
59
return ;
8
60
}
9
61
10
- const el = tech . el ( ) ;
62
+ // there is only one valid source element url
63
+ // use that
64
+ if ( srcUrls . length === 1 ) {
65
+ src = srcUrls [ 0 ] ;
66
+ }
11
67
12
- // we need to fire sourceset when the player is ready
13
- // if we find that the media element had a src when it was
14
- // given to us and that tech element is not in a stalled state
15
- if ( el . src || el . currentSrc && tech . el ( ) . initNetworkState_ !== 3 ) {
16
- tech . triggerSourceset ( el . src || el . currentSrc ) ;
68
+ tech . triggerSourceset ( src ) ;
69
+ return true ;
70
+ } ;
71
+
72
+ /**
73
+ * Get the browsers property descriptor for the `innerHTML`
74
+ * property. This will allow us to overwrite it without
75
+ * destroying native functionality.
76
+ *
77
+ * @param {HTMLMediaElement } el
78
+ * The tech element that should be used to get the descriptor
79
+ *
80
+ * @return {Object }
81
+ * The property descriptor for innerHTML.
82
+ */
83
+ const getInnerHTMLDescriptor = ( el ) => {
84
+ const proto = window . Element . prototype ;
85
+ let innerDescriptor = { } ;
86
+
87
+ // preserve getters/setters already on `el.innerHTML` if they exist
88
+ if ( Object . getOwnPropertyDescriptor ( el , 'innerHTML' ) ) {
89
+ innerDescriptor = Object . getOwnPropertyDescriptor ( el , 'innerHTML' ) ;
90
+ } else if ( Object . getOwnPropertyDescriptor ( proto , 'innerHTML' ) ) {
91
+ innerDescriptor = Object . getOwnPropertyDescriptor ( proto , 'innerHTML' ) ;
92
+ }
93
+
94
+ if ( ! innerDescriptor . get ) {
95
+ innerDescriptor . get = function ( ) {
96
+ return el . cloneNode ( ) . innerHTML ;
97
+ } ;
98
+ }
99
+
100
+ if ( ! innerDescriptor . set ) {
101
+ innerDescriptor . set = function ( v ) {
102
+ // remove all current content from inside
103
+ el . innerText = '' ;
104
+
105
+ // make a dummy node to use innerHTML on
106
+ const dummy = document . createElement ( el . nodeName . toLowerCase ( ) ) ;
107
+
108
+ // set innerHTML to the value provided
109
+ dummy . innerHTML = v ;
110
+
111
+ // make a document fragment to hold the nodes from dummy
112
+ const docFrag = document . createDocumentFragment ( ) ;
113
+
114
+ // copy all of the nodes created by the innerHTML on dummy
115
+ // to the document fragment
116
+ while ( dummy . childNodes . length ) {
117
+ docFrag . appendChild ( dummy . childNodes [ 0 ] ) ;
118
+ }
119
+
120
+ // now we add all of that html in one by appending the
121
+ // document fragment. This is how innerHTML does it.
122
+ window . Element . prototype . appendChild . call ( el , docFrag ) ;
123
+
124
+ // then return the result that innerHTML's setter would
125
+ return el . innerHTML ;
126
+ } ;
17
127
}
18
128
129
+ if ( typeof innerDescriptor . enumerable === 'undefined' ) {
130
+ innerDescriptor . enumerable = true ;
131
+ }
132
+
133
+ innerDescriptor . configurable = true ;
134
+
135
+ return innerDescriptor ;
136
+ } ;
137
+
138
+ /**
139
+ * Get the browsers property descriptor for the `src`
140
+ * property. This will allow us to overwrite it without
141
+ * destroying native functionality.
142
+ *
143
+ * @param {HTMLMediaElement } el
144
+ * The tech element that should be used to get the descriptor
145
+ *
146
+ * @return {Object }
147
+ * The property descriptor for `src`.
148
+ */
149
+ const getSrcDescriptor = ( el ) => {
19
150
const proto = window . HTMLMediaElement . prototype ;
20
151
let srcDescriptor = { } ;
21
152
@@ -42,42 +173,185 @@ const setupSourceset = function(tech) {
42
173
srcDescriptor . enumerable = true ;
43
174
}
44
175
176
+ srcDescriptor . configurable = true ;
177
+
178
+ return srcDescriptor ;
179
+ } ;
180
+
181
+ /**
182
+ * Patches browser internal functions so that we can tell syncronously
183
+ * if a `<source>` was appended to the media element. For some reason this
184
+ * causes a `sourceset` if the the media element is ready and has no source.
185
+ * This happens when:
186
+ * - The page has just loaded and the media element does not have a source.
187
+ * - The media element was emptied of all sources, then `load()` was called.
188
+ *
189
+ * It does this by patching the following functions/properties when they are supported:
190
+ *
191
+ * - `append()` - can be used to add a `<source>` element to the media element
192
+ * - `appendChild()` - can be used to add a `<source>` element to the media element
193
+ * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element
194
+ * - `innerHTML` - can be used to add a `<source>` element to the media element
195
+ *
196
+ * @param {Html5 } tech
197
+ * The tech object that sourceset is being setup on.
198
+ */
199
+ const firstSourceWatch = function ( tech ) {
200
+ const el = tech . el ( ) ;
201
+
202
+ // make sure firstSourceWatch isn't setup twice.
203
+ if ( el . firstSourceWatch_ ) {
204
+ return ;
205
+ }
206
+
207
+ el . firstSourceWatch_ = true ;
208
+ const oldAppend = el . append ;
209
+ const oldAppendChild = el . appendChild ;
210
+ const oldInsertAdjacentHTML = el . insertAdjacentHTML ;
211
+ const innerDescriptor = getInnerHTMLDescriptor ( el ) ;
212
+
213
+ el . appendChild = function ( ) {
214
+ const retval = oldAppendChild . apply ( el , arguments ) ;
215
+
216
+ sourcesetLoad ( tech ) ;
217
+
218
+ return retval ;
219
+ } ;
220
+
221
+ if ( oldAppend ) {
222
+ el . append = function ( ) {
223
+ const retval = oldAppend . apply ( el , arguments ) ;
224
+
225
+ sourcesetLoad ( tech ) ;
226
+
227
+ return retval ;
228
+ } ;
229
+ }
230
+
231
+ if ( oldInsertAdjacentHTML ) {
232
+ el . insertAdjacentHTML = function ( ) {
233
+ const retval = oldInsertAdjacentHTML . apply ( el , arguments ) ;
234
+
235
+ sourcesetLoad ( tech ) ;
236
+
237
+ return retval ;
238
+ } ;
239
+ }
240
+
241
+ Object . defineProperty ( el , 'innerHTML' , {
242
+ get : innerDescriptor . get . bind ( el ) ,
243
+ set ( v ) {
244
+ const retval = innerDescriptor . set . call ( el , v ) ;
245
+
246
+ sourcesetLoad ( tech ) ;
247
+
248
+ return retval ;
249
+ } ,
250
+ configurable : true ,
251
+ enumerable : innerDescriptor . enumerable
252
+ } ) ;
253
+
254
+ // on the first sourceset, we need to revert
255
+ // our changes
256
+ tech . one ( 'sourceset' , ( e ) => {
257
+ el . firstSourceWatch_ = false ;
258
+ el . appendChild = oldAppendChild ;
259
+
260
+ if ( oldAppend ) {
261
+ el . append = oldAppend ;
262
+ }
263
+ if ( oldInsertAdjacentHTML ) {
264
+ el . insertAdjacentHTML = oldInsertAdjacentHTML ;
265
+ }
266
+
267
+ Object . defineProperty ( el , 'innerHTML' , innerDescriptor ) ;
268
+ } ) ;
269
+ } ;
270
+
271
+ /**
272
+ * setup `sourceset` handling on the `Html5` tech. This function
273
+ * patches the following element properties/functions:
274
+ *
275
+ * - `src` - to determine when `src` is set
276
+ * - `setAttribute()` - to determine when `src` is set
277
+ * - `load()` - this re-triggers the source selection algorithm, and can
278
+ * cause a sourceset.
279
+ *
280
+ * If there is no source when we are adding `sourceset` support or during a `load()`
281
+ * we also patch the functions listed in `firstSourceWatch`.
282
+ *
283
+ * @param {Html5 } tech
284
+ * The tech to patch
285
+ */
286
+ const setupSourceset = function ( tech ) {
287
+ if ( ! tech . featuresSourceset ) {
288
+ return ;
289
+ }
290
+
291
+ const el = tech . el ( ) ;
292
+
293
+ // make sure sourceset isn't setup twice.
294
+ if ( el . setupSourceset_ ) {
295
+ return ;
296
+ }
297
+
298
+ el . setupSourceset_ = true ;
299
+
300
+ const srcDescriptor = getSrcDescriptor ( el ) ;
301
+ const oldSetAttribute = el . setAttribute ;
302
+ const oldLoad = el . load ;
303
+
304
+ // we need to fire sourceset when the player is ready
305
+ // if we find that the media element had a src when it was
306
+ // given to us and that tech element is not in a stalled state
307
+ if ( el . src || el . currentSrc && el . initNetworkState_ !== 3 ) {
308
+ if ( el . currentSrc ) {
309
+ tech . triggerSourceset ( el . currentSrc ) ;
310
+ } else {
311
+ sourcesetLoad ( tech ) ;
312
+ }
313
+ }
314
+
315
+ // for some reason adding a source element when a mediaElement has no source
316
+ // calls `load` internally right away. We need to handle that.
317
+ if ( ! el . src && ! el . currentSrc && ! tech . $$ ( 'source' ) . length ) {
318
+ firstSourceWatch ( tech ) ;
319
+ }
320
+
45
321
Object . defineProperty ( el , 'src' , {
46
322
get : srcDescriptor . get . bind ( el ) ,
47
323
set : ( v ) => {
48
324
const retval = srcDescriptor . set . call ( el , v ) ;
49
325
50
- tech . triggerSourceset ( v ) ;
326
+ // we use the getter here to get the actual value set on src
327
+ tech . triggerSourceset ( el . src ) ;
51
328
52
329
return retval ;
53
330
} ,
54
331
configurable : true ,
55
332
enumerable : srcDescriptor . enumerable
56
333
} ) ;
57
334
58
- const oldSetAttribute = el . setAttribute ;
59
-
60
335
el . setAttribute = ( n , v ) => {
61
336
const retval = oldSetAttribute . call ( el , n , v ) ;
62
337
63
338
if ( n === 'src' ) {
64
- tech . triggerSourceset ( v ) ;
339
+ tech . triggerSourceset ( el . getAttribute ( 'src' ) ) ;
65
340
}
66
341
67
342
return retval ;
68
343
} ;
69
344
70
- const oldLoad = el . load ;
71
-
72
345
el . load = ( ) => {
73
346
const retval = oldLoad . call ( el ) ;
74
347
75
- // if `el.src` is set, that source will be loaded
76
- // otherwise, we can't know for sure what source will be set because
77
- // source elements will be used but implementing the source selection algorithm
78
- // is laborious and asynchronous, so,
79
- // instead return an empty string to basically indicate source may change
80
- tech . triggerSourceset ( el . src || '' ) ;
348
+ // if load was called, but there was no source to fire
349
+ // sourceset on. We have to watch for a source append
350
+ // as that can trigger a `sourceset` when the media element
351
+ // has no source
352
+ if ( ! sourcesetLoad ( tech ) ) {
353
+ firstSourceWatch ( tech ) ;
354
+ }
81
355
82
356
return retval ;
83
357
} ;
0 commit comments