mirror of
				https://github.com/videojs/video.js.git
				synced 2025-10-31 00:08:01 +02:00 
			
		
		
		
	feat: Add loadMedia and getMedia methods (#5652)
`loadMedia` accepts a MediaObject and an optional ready handler. It'll reset the player -- including text tracks, poster, and source -- before setting the new provided media, which include sources, poster, text tracks. `getMedia` will return either the provided media object or the currently set values for sources, text tracks, and poster. Fixes #4342
This commit is contained in:
		
				
					committed by
					
						 Gary Katsevman
						Gary Katsevman
					
				
			
			
				
	
			
			
			
						parent
						
							3d093ede98
						
					
				
				
					commit
					874cc21a4e
				
			
							
								
								
									
										130
									
								
								src/js/player.js
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								src/js/player.js
									
									
									
									
									
								
							| @@ -32,7 +32,7 @@ import Tech from './tech/tech.js'; | ||||
| import * as middleware from './tech/middleware.js'; | ||||
| import {ALL as TRACK_TYPES} from './tracks/track-types'; | ||||
| import filterSource from './utils/filter-source'; | ||||
| import {findMimetype} from './utils/mimetypes'; | ||||
| import {getMimetype, findMimetype} from './utils/mimetypes'; | ||||
| import {IE_VERSION} from './utils/browser'; | ||||
|  | ||||
| // The following imports are used only to ensure that the corresponding modules | ||||
| @@ -2931,6 +2931,10 @@ class Player extends Component { | ||||
|     if (this.tech_) { | ||||
|       this.tech_.clearTracks('text'); | ||||
|     } | ||||
|     if (this.cache_) { | ||||
|       this.cache_.media = null; | ||||
|     } | ||||
|     this.poster(''); | ||||
|     this.loadTech_(this.options_.techOrder[0], null); | ||||
|     this.techCall_('reset'); | ||||
|     if (isEvented(this)) { | ||||
| @@ -3952,6 +3956,130 @@ class Player extends Component { | ||||
|     return BREAKPOINT_CLASSES[this.breakpoint_] || ''; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * An object that describes a single piece of media. | ||||
|    * | ||||
|    * Properties that are not part of this type description will be retained; so, | ||||
|    * this can be viewed as a generic metadata storage mechanism as well. | ||||
|    * | ||||
|    * @see      {@link https://wicg.github.io/mediasession/#the-mediametadata-interface} | ||||
|    * @typedef  {Object} Player~MediaObject | ||||
|    * | ||||
|    * @property {string} [album] | ||||
|    *           Unused, except if this object is passed to the `MediaSession` | ||||
|    *           API. | ||||
|    * | ||||
|    * @property {string} [artist] | ||||
|    *           Unused, except if this object is passed to the `MediaSession` | ||||
|    *           API. | ||||
|    * | ||||
|    * @property {Object[]} [artwork] | ||||
|    *           Unused, except if this object is passed to the `MediaSession` | ||||
|    *           API. If not specified, will be populated via the `poster`, if | ||||
|    *           available. | ||||
|    * | ||||
|    * @property {string} [poster] | ||||
|    *           URL to an image that will display before playback. | ||||
|    * | ||||
|    * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src] | ||||
|    *           A single source object, an array of source objects, or a string | ||||
|    *           referencing a URL to a media source. It is _highly recommended_ | ||||
|    *           that an object or array of objects is used here, so that source | ||||
|    *           selection algorithms can take the `type` into account. | ||||
|    * | ||||
|    * @property {string} [title] | ||||
|    *           Unused, except if this object is passed to the `MediaSession` | ||||
|    *           API. | ||||
|    * | ||||
|    * @property {Object[]} [textTracks] | ||||
|    *           An array of objects to be used to create text tracks, following | ||||
|    *           the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}. | ||||
|    *           For ease of removal, these will be created as "remote" text | ||||
|    *           tracks and set to automatically clean up on source changes. | ||||
|    * | ||||
|    *           These objects may have properties like `src`, `kind`, `label`, | ||||
|    *           and `language`, see {@link Tech#createRemoteTextTrack}. | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|    * Populate the player using a {@link Player~MediaObject|MediaObject}. | ||||
|    * | ||||
|    * @param  {Player~MediaObject} media | ||||
|    *         A media object. | ||||
|    * | ||||
|    * @param  {Function} ready | ||||
|    *         A callback to be called when the player is ready. | ||||
|    */ | ||||
|   loadMedia(media, ready) { | ||||
|     if (!media || typeof media !== 'object') { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this.reset(); | ||||
|  | ||||
|     // Clone the media object so it cannot be mutated from outside. | ||||
|     this.cache_.media = mergeOptions(media); | ||||
|  | ||||
|     const {artwork, poster, src, textTracks} = this.cache_.media; | ||||
|  | ||||
|     // If `artwork` is not given, create it using `poster`. | ||||
|     if (!artwork && poster) { | ||||
|       this.cache_.media.artwork = [{ | ||||
|         src: poster, | ||||
|         type: getMimetype(poster) | ||||
|       }]; | ||||
|     } | ||||
|  | ||||
|     if (src) { | ||||
|       this.src(src); | ||||
|     } | ||||
|  | ||||
|     if (poster) { | ||||
|       this.poster(poster); | ||||
|     } | ||||
|  | ||||
|     if (Array.isArray(textTracks)) { | ||||
|       textTracks.forEach(tt => this.addRemoteTextTrack(tt, false)); | ||||
|     } | ||||
|  | ||||
|     this.ready(ready); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get a clone of the current {@link Player~MediaObject} for this player. | ||||
|    * | ||||
|    * If the `loadMedia` method has not been used, will attempt to return a | ||||
|    * {@link Player~MediaObject} based on the current state of the player. | ||||
|    * | ||||
|    * @return {Player~MediaObject} | ||||
|    */ | ||||
|   getMedia() { | ||||
|     if (!this.cache_.media) { | ||||
|       const poster = this.poster(); | ||||
|       const src = this.currentSources(); | ||||
|       const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({ | ||||
|         kind: tt.kind, | ||||
|         label: tt.label, | ||||
|         language: tt.language, | ||||
|         src: tt.src | ||||
|       })); | ||||
|  | ||||
|       const media = {src, textTracks}; | ||||
|  | ||||
|       if (poster) { | ||||
|         media.poster = poster; | ||||
|         media.artwork = [{ | ||||
|           src: media.poster, | ||||
|           type: getMimetype(media.poster) | ||||
|         }]; | ||||
|       } | ||||
|  | ||||
|       return media; | ||||
|     } | ||||
|  | ||||
|     return mergeOptions(this.cache_.media); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Gets tag settings | ||||
|    * | ||||
|   | ||||
| @@ -17,7 +17,13 @@ export const MimetypesKind = { | ||||
|   mp3: 'audio/mpeg', | ||||
|   aac: 'audio/aac', | ||||
|   oga: 'audio/ogg', | ||||
|   m3u8: 'application/x-mpegURL' | ||||
|   m3u8: 'application/x-mpegURL', | ||||
|   jpg: 'image/jpeg', | ||||
|   jpeg: 'image/jpeg', | ||||
|   gif: 'image/gif', | ||||
|   png: 'image/png', | ||||
|   svg: 'image/svg+xml', | ||||
|   webp: 'image/webp' | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
							
								
								
									
										201
									
								
								test/unit/player-loadmedia.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								test/unit/player-loadmedia.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,201 @@ | ||||
| /* eslint-env qunit */ | ||||
| import TestHelpers from './test-helpers'; | ||||
|  | ||||
| QUnit.module('Player: loadMedia/getMedia', { | ||||
|  | ||||
|   beforeEach() { | ||||
|     this.player = TestHelpers.makePlayer({}); | ||||
|   }, | ||||
|  | ||||
|   afterEach() { | ||||
|     this.player.dispose(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia sets source from a string', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     src: 'foo.mp4' | ||||
|   }); | ||||
|  | ||||
|   assert.strictEqual(this.player.currentSrc(), 'foo.mp4', 'currentSrc was correct'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia sets source from an object', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     src: { | ||||
|       src: 'foo.mp4', | ||||
|       type: 'video/mp4' | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   assert.strictEqual(this.player.currentSrc(), 'foo.mp4', 'currentSrc was correct'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia sets source from an array', function(assert) { | ||||
|   const sources = [{ | ||||
|     src: 'foo.mp4', | ||||
|     type: 'video/mp4' | ||||
|   }, { | ||||
|     src: 'foo.webm', | ||||
|     type: 'video/webm' | ||||
|   }]; | ||||
|  | ||||
|   this.player.loadMedia({ | ||||
|     src: sources | ||||
|   }); | ||||
|  | ||||
|   assert.strictEqual(this.player.currentSrc(), sources[0].src, 'currentSrc was correct'); | ||||
|   assert.deepEqual(this.player.currentSource(), sources[0], 'currentSource was correct'); | ||||
|   assert.deepEqual(this.player.currentSources(), sources, 'currentSources were correct'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia sets poster and backfills artwork', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     poster: 'foo.jpg' | ||||
|   }); | ||||
|  | ||||
|   assert.strictEqual(this.player.poster(), 'foo.jpg', 'poster was correct'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia sets artwork via poster', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     poster: 'foo.jpg' | ||||
|   }); | ||||
|  | ||||
|   const {artwork} = this.player.getMedia(); | ||||
|  | ||||
|   assert.deepEqual(artwork, [{ | ||||
|     src: 'foo.jpg', | ||||
|     type: 'image/jpeg' | ||||
|   }], 'the artwork was set to match the poster'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia sets artwork and poster independently', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     poster: 'foo.jpg', | ||||
|     artwork: [{ | ||||
|       src: 'bar.png', | ||||
|       type: 'image/png' | ||||
|     }] | ||||
|   }); | ||||
|  | ||||
|   assert.strictEqual(this.player.poster(), 'foo.jpg', 'poster was correct'); | ||||
|   assert.deepEqual(this.player.getMedia().artwork, [{ | ||||
|     src: 'bar.png', | ||||
|     type: 'image/png' | ||||
|   }], 'the artwork was provided, so does not match poster'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('loadMedia creates text tracks', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     textTracks: [{ | ||||
|       kind: 'captions', | ||||
|       src: 'foo.vtt', | ||||
|       language: 'en', | ||||
|       label: 'English' | ||||
|     }] | ||||
|   }); | ||||
|  | ||||
|   const rtt = this.player.remoteTextTracks()[0]; | ||||
|  | ||||
|   assert.ok(Boolean(rtt), 'the track exists'); | ||||
|   assert.strictEqual(rtt.kind, 'captions', 'the kind is correct'); | ||||
|   assert.strictEqual(rtt.src, 'foo.vtt', 'the src is correct'); | ||||
|   assert.strictEqual(rtt.language, 'en', 'the language is correct'); | ||||
|   assert.strictEqual(rtt.label, 'English', 'the label is correct'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('getMedia returns a clone of the media object', function(assert) { | ||||
|   const original = { | ||||
|     arbitrary: true, | ||||
|     src: 'foo.mp4', | ||||
|     poster: 'foo.gif', | ||||
|     textTracks: [{ | ||||
|       kind: 'captions', | ||||
|       src: 'foo.vtt', | ||||
|       language: 'en', | ||||
|       label: 'English' | ||||
|     }] | ||||
|   }; | ||||
|  | ||||
|   this.player.loadMedia(original); | ||||
|  | ||||
|   const result = this.player.getMedia(); | ||||
|  | ||||
|   assert.notStrictEqual(result, original, 'a new object is returned'); | ||||
|   assert.deepEqual(result, { | ||||
|     arbitrary: true, | ||||
|     artwork: [{ | ||||
|       src: 'foo.gif', | ||||
|       type: 'image/gif' | ||||
|     }], | ||||
|     src: 'foo.mp4', | ||||
|     poster: 'foo.gif', | ||||
|     textTracks: [{ | ||||
|       kind: 'captions', | ||||
|       src: 'foo.vtt', | ||||
|       language: 'en', | ||||
|       label: 'English' | ||||
|     }] | ||||
|   }, 'the object has the expected structure'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('getMedia returns a new media object when no media has been loaded', function(assert) { | ||||
|  | ||||
|   this.player.poster = () => 'foo.gif'; | ||||
|   this.player.currentSources = () => [{src: 'foo.mp4', type: 'video/mp4'}]; | ||||
|   this.player.remoteTextTracks = () => [{ | ||||
|     kind: 'captions', | ||||
|     src: 'foo.vtt', | ||||
|     language: 'en', | ||||
|     label: 'English' | ||||
|   }, { | ||||
|     kind: 'subtitles', | ||||
|     src: 'bar.vtt', | ||||
|     language: 'de', | ||||
|     label: 'German' | ||||
|   }]; | ||||
|  | ||||
|   const result = this.player.getMedia(); | ||||
|  | ||||
|   assert.deepEqual(result, { | ||||
|     artwork: [{ | ||||
|       src: 'foo.gif', | ||||
|       type: 'image/gif' | ||||
|     }], | ||||
|     src: [{ | ||||
|       src: 'foo.mp4', | ||||
|       type: 'video/mp4' | ||||
|     }], | ||||
|     poster: 'foo.gif', | ||||
|     textTracks: [{ | ||||
|       kind: 'captions', | ||||
|       src: 'foo.vtt', | ||||
|       language: 'en', | ||||
|       label: 'English' | ||||
|     }, { | ||||
|       kind: 'subtitles', | ||||
|       src: 'bar.vtt', | ||||
|       language: 'de', | ||||
|       label: 'German' | ||||
|     }] | ||||
|   }, 'the object has the expected structure'); | ||||
| }); | ||||
|  | ||||
| // This only tests the relevant aspect of the reset function. The rest of its | ||||
| // effects are tested in player.test.js | ||||
| QUnit.test('reset discards the media object', function(assert) { | ||||
|   this.player.loadMedia({ | ||||
|     poster: 'foo.jpg', | ||||
|     src: 'foo.mp4', | ||||
|     textTracks: [{src: 'foo.vtt'}] | ||||
|   }); | ||||
|  | ||||
|   this.player.reset(); | ||||
|  | ||||
|   // TODO: There is a bug with player.reset() where it does not clear internal | ||||
|   // cachces completely. Remove this when that's fixed. | ||||
|   this.player.cache_.sources = []; | ||||
|  | ||||
|   assert.deepEqual(this.player.getMedia(), {src: [], textTracks: []}, 'any empty media object is returned'); | ||||
| }); | ||||
| @@ -1426,7 +1426,8 @@ QUnit.test('player#reset loads the Html5 tech and then techCalls reset', functio | ||||
|     }, | ||||
|     techCall_(method) { | ||||
|       techCallMethod = method; | ||||
|     } | ||||
|     }, | ||||
|     poster() {} | ||||
|   }; | ||||
|  | ||||
|   Player.prototype.reset.call(testPlayer); | ||||
| @@ -1451,7 +1452,8 @@ QUnit.test('player#reset loads the first item in the techOrder and then techCall | ||||
|     }, | ||||
|     techCall_(method) { | ||||
|       techCallMethod = method; | ||||
|     } | ||||
|     }, | ||||
|     poster() {} | ||||
|   }; | ||||
|  | ||||
|   Player.prototype.reset.call(testPlayer); | ||||
| @@ -1461,6 +1463,34 @@ QUnit.test('player#reset loads the first item in the techOrder and then techCall | ||||
|   assert.equal(techCallMethod, 'reset', 'we then reset the tech'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('player#reset removes the poster', function(assert) { | ||||
|   const player = TestHelpers.makePlayer(); | ||||
|  | ||||
|   this.clock.tick(1); | ||||
|  | ||||
|   player.poster('foo.jpg'); | ||||
|   assert.strictEqual(player.poster(), 'foo.jpg', 'the poster was set'); | ||||
|   player.reset(); | ||||
|   assert.strictEqual(player.poster(), '', 'the poster was reset'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('player#reset removes remote text tracks', function(assert) { | ||||
|   const player = TestHelpers.makePlayer(); | ||||
|  | ||||
|   this.clock.tick(1); | ||||
|  | ||||
|   player.addRemoteTextTrack({ | ||||
|     kind: 'captions', | ||||
|     src: 'foo.vtt', | ||||
|     language: 'en', | ||||
|     label: 'English' | ||||
|   }); | ||||
|  | ||||
|   assert.strictEqual(player.remoteTextTracks().length, 1, 'there is one RTT'); | ||||
|   player.reset(); | ||||
|   assert.strictEqual(player.remoteTextTracks().length, 0, 'there are zero RTTs'); | ||||
| }); | ||||
|  | ||||
| QUnit.test('Remove waiting class after tech waiting when timeupdate shows a time change', function(assert) { | ||||
|   const player = TestHelpers.makePlayer(); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user