mirror of
https://github.com/videojs/video.js.git
synced 2024-12-27 02:43:45 +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:
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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user