1
0
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:
Pat O'Neill 2019-01-03 13:49:34 -05:00 committed by Gary Katsevman
parent 3d093ede98
commit 874cc21a4e
4 changed files with 369 additions and 4 deletions

View File

@ -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
*

View File

@ -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'
};
/**

View 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');
});

View File

@ -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();