Skip to content

Commit 874cc21

Browse files
misteroneillgkatsev
authored andcommitted
feat: Add loadMedia and getMedia methods (videojs#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 videojs#4342
1 parent 3d093ed commit 874cc21

4 files changed

Lines changed: 369 additions & 4 deletions

File tree

src/js/player.js

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import Tech from './tech/tech.js';
3232
import * as middleware from './tech/middleware.js';
3333
import {ALL as TRACK_TYPES} from './tracks/track-types';
3434
import filterSource from './utils/filter-source';
35-
import {findMimetype} from './utils/mimetypes';
35+
import {getMimetype, findMimetype} from './utils/mimetypes';
3636
import {IE_VERSION} from './utils/browser';
3737

3838
// The following imports are used only to ensure that the corresponding modules
@@ -2931,6 +2931,10 @@ class Player extends Component {
29312931
if (this.tech_) {
29322932
this.tech_.clearTracks('text');
29332933
}
2934+
if (this.cache_) {
2935+
this.cache_.media = null;
2936+
}
2937+
this.poster('');
29342938
this.loadTech_(this.options_.techOrder[0], null);
29352939
this.techCall_('reset');
29362940
if (isEvented(this)) {
@@ -3952,6 +3956,130 @@ class Player extends Component {
39523956
return BREAKPOINT_CLASSES[this.breakpoint_] || '';
39533957
}
39543958

3959+
/**
3960+
* An object that describes a single piece of media.
3961+
*
3962+
* Properties that are not part of this type description will be retained; so,
3963+
* this can be viewed as a generic metadata storage mechanism as well.
3964+
*
3965+
* @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
3966+
* @typedef {Object} Player~MediaObject
3967+
*
3968+
* @property {string} [album]
3969+
* Unused, except if this object is passed to the `MediaSession`
3970+
* API.
3971+
*
3972+
* @property {string} [artist]
3973+
* Unused, except if this object is passed to the `MediaSession`
3974+
* API.
3975+
*
3976+
* @property {Object[]} [artwork]
3977+
* Unused, except if this object is passed to the `MediaSession`
3978+
* API. If not specified, will be populated via the `poster`, if
3979+
* available.
3980+
*
3981+
* @property {string} [poster]
3982+
* URL to an image that will display before playback.
3983+
*
3984+
* @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
3985+
* A single source object, an array of source objects, or a string
3986+
* referencing a URL to a media source. It is _highly recommended_
3987+
* that an object or array of objects is used here, so that source
3988+
* selection algorithms can take the `type` into account.
3989+
*
3990+
* @property {string} [title]
3991+
* Unused, except if this object is passed to the `MediaSession`
3992+
* API.
3993+
*
3994+
* @property {Object[]} [textTracks]
3995+
* An array of objects to be used to create text tracks, following
3996+
* the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
3997+
* For ease of removal, these will be created as "remote" text
3998+
* tracks and set to automatically clean up on source changes.
3999+
*
4000+
* These objects may have properties like `src`, `kind`, `label`,
4001+
* and `language`, see {@link Tech#createRemoteTextTrack}.
4002+
*/
4003+
4004+
/**
4005+
* Populate the player using a {@link Player~MediaObject|MediaObject}.
4006+
*
4007+
* @param {Player~MediaObject} media
4008+
* A media object.
4009+
*
4010+
* @param {Function} ready
4011+
* A callback to be called when the player is ready.
4012+
*/
4013+
loadMedia(media, ready) {
4014+
if (!media || typeof media !== 'object') {
4015+
return;
4016+
}
4017+
4018+
this.reset();
4019+
4020+
// Clone the media object so it cannot be mutated from outside.
4021+
this.cache_.media = mergeOptions(media);
4022+
4023+
const {artwork, poster, src, textTracks} = this.cache_.media;
4024+
4025+
// If `artwork` is not given, create it using `poster`.
4026+
if (!artwork && poster) {
4027+
this.cache_.media.artwork = [{
4028+
src: poster,
4029+
type: getMimetype(poster)
4030+
}];
4031+
}
4032+
4033+
if (src) {
4034+
this.src(src);
4035+
}
4036+
4037+
if (poster) {
4038+
this.poster(poster);
4039+
}
4040+
4041+
if (Array.isArray(textTracks)) {
4042+
textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
4043+
}
4044+
4045+
this.ready(ready);
4046+
}
4047+
4048+
/**
4049+
* Get a clone of the current {@link Player~MediaObject} for this player.
4050+
*
4051+
* If the `loadMedia` method has not been used, will attempt to return a
4052+
* {@link Player~MediaObject} based on the current state of the player.
4053+
*
4054+
* @return {Player~MediaObject}
4055+
*/
4056+
getMedia() {
4057+
if (!this.cache_.media) {
4058+
const poster = this.poster();
4059+
const src = this.currentSources();
4060+
const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({
4061+
kind: tt.kind,
4062+
label: tt.label,
4063+
language: tt.language,
4064+
src: tt.src
4065+
}));
4066+
4067+
const media = {src, textTracks};
4068+
4069+
if (poster) {
4070+
media.poster = poster;
4071+
media.artwork = [{
4072+
src: media.poster,
4073+
type: getMimetype(media.poster)
4074+
}];
4075+
}
4076+
4077+
return media;
4078+
}
4079+
4080+
return mergeOptions(this.cache_.media);
4081+
}
4082+
39554083
/**
39564084
* Gets tag settings
39574085
*

src/js/utils/mimetypes.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export const MimetypesKind = {
1717
mp3: 'audio/mpeg',
1818
aac: 'audio/aac',
1919
oga: 'audio/ogg',
20-
m3u8: 'application/x-mpegURL'
20+
m3u8: 'application/x-mpegURL',
21+
jpg: 'image/jpeg',
22+
jpeg: 'image/jpeg',
23+
gif: 'image/gif',
24+
png: 'image/png',
25+
svg: 'image/svg+xml',
26+
webp: 'image/webp'
2127
};
2228

2329
/**

test/unit/player-loadmedia.test.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/* eslint-env qunit */
2+
import TestHelpers from './test-helpers';
3+
4+
QUnit.module('Player: loadMedia/getMedia', {
5+
6+
beforeEach() {
7+
this.player = TestHelpers.makePlayer({});
8+
},
9+
10+
afterEach() {
11+
this.player.dispose();
12+
}
13+
});
14+
15+
QUnit.test('loadMedia sets source from a string', function(assert) {
16+
this.player.loadMedia({
17+
src: 'foo.mp4'
18+
});
19+
20+
assert.strictEqual(this.player.currentSrc(), 'foo.mp4', 'currentSrc was correct');
21+
});
22+
23+
QUnit.test('loadMedia sets source from an object', function(assert) {
24+
this.player.loadMedia({
25+
src: {
26+
src: 'foo.mp4',
27+
type: 'video/mp4'
28+
}
29+
});
30+
31+
assert.strictEqual(this.player.currentSrc(), 'foo.mp4', 'currentSrc was correct');
32+
});
33+
34+
QUnit.test('loadMedia sets source from an array', function(assert) {
35+
const sources = [{
36+
src: 'foo.mp4',
37+
type: 'video/mp4'
38+
}, {
39+
src: 'foo.webm',
40+
type: 'video/webm'
41+
}];
42+
43+
this.player.loadMedia({
44+
src: sources
45+
});
46+
47+
assert.strictEqual(this.player.currentSrc(), sources[0].src, 'currentSrc was correct');
48+
assert.deepEqual(this.player.currentSource(), sources[0], 'currentSource was correct');
49+
assert.deepEqual(this.player.currentSources(), sources, 'currentSources were correct');
50+
});
51+
52+
QUnit.test('loadMedia sets poster and backfills artwork', function(assert) {
53+
this.player.loadMedia({
54+
poster: 'foo.jpg'
55+
});
56+
57+
assert.strictEqual(this.player.poster(), 'foo.jpg', 'poster was correct');
58+
});
59+
60+
QUnit.test('loadMedia sets artwork via poster', function(assert) {
61+
this.player.loadMedia({
62+
poster: 'foo.jpg'
63+
});
64+
65+
const {artwork} = this.player.getMedia();
66+
67+
assert.deepEqual(artwork, [{
68+
src: 'foo.jpg',
69+
type: 'image/jpeg'
70+
}], 'the artwork was set to match the poster');
71+
});
72+
73+
QUnit.test('loadMedia sets artwork and poster independently', function(assert) {
74+
this.player.loadMedia({
75+
poster: 'foo.jpg',
76+
artwork: [{
77+
src: 'bar.png',
78+
type: 'image/png'
79+
}]
80+
});
81+
82+
assert.strictEqual(this.player.poster(), 'foo.jpg', 'poster was correct');
83+
assert.deepEqual(this.player.getMedia().artwork, [{
84+
src: 'bar.png',
85+
type: 'image/png'
86+
}], 'the artwork was provided, so does not match poster');
87+
});
88+
89+
QUnit.test('loadMedia creates text tracks', function(assert) {
90+
this.player.loadMedia({
91+
textTracks: [{
92+
kind: 'captions',
93+
src: 'foo.vtt',
94+
language: 'en',
95+
label: 'English'
96+
}]
97+
});
98+
99+
const rtt = this.player.remoteTextTracks()[0];
100+
101+
assert.ok(Boolean(rtt), 'the track exists');
102+
assert.strictEqual(rtt.kind, 'captions', 'the kind is correct');
103+
assert.strictEqual(rtt.src, 'foo.vtt', 'the src is correct');
104+
assert.strictEqual(rtt.language, 'en', 'the language is correct');
105+
assert.strictEqual(rtt.label, 'English', 'the label is correct');
106+
});
107+
108+
QUnit.test('getMedia returns a clone of the media object', function(assert) {
109+
const original = {
110+
arbitrary: true,
111+
src: 'foo.mp4',
112+
poster: 'foo.gif',
113+
textTracks: [{
114+
kind: 'captions',
115+
src: 'foo.vtt',
116+
language: 'en',
117+
label: 'English'
118+
}]
119+
};
120+
121+
this.player.loadMedia(original);
122+
123+
const result = this.player.getMedia();
124+
125+
assert.notStrictEqual(result, original, 'a new object is returned');
126+
assert.deepEqual(result, {
127+
arbitrary: true,
128+
artwork: [{
129+
src: 'foo.gif',
130+
type: 'image/gif'
131+
}],
132+
src: 'foo.mp4',
133+
poster: 'foo.gif',
134+
textTracks: [{
135+
kind: 'captions',
136+
src: 'foo.vtt',
137+
language: 'en',
138+
label: 'English'
139+
}]
140+
}, 'the object has the expected structure');
141+
});
142+
143+
QUnit.test('getMedia returns a new media object when no media has been loaded', function(assert) {
144+
145+
this.player.poster = () => 'foo.gif';
146+
this.player.currentSources = () => [{src: 'foo.mp4', type: 'video/mp4'}];
147+
this.player.remoteTextTracks = () => [{
148+
kind: 'captions',
149+
src: 'foo.vtt',
150+
language: 'en',
151+
label: 'English'
152+
}, {
153+
kind: 'subtitles',
154+
src: 'bar.vtt',
155+
language: 'de',
156+
label: 'German'
157+
}];
158+
159+
const result = this.player.getMedia();
160+
161+
assert.deepEqual(result, {
162+
artwork: [{
163+
src: 'foo.gif',
164+
type: 'image/gif'
165+
}],
166+
src: [{
167+
src: 'foo.mp4',
168+
type: 'video/mp4'
169+
}],
170+
poster: 'foo.gif',
171+
textTracks: [{
172+
kind: 'captions',
173+
src: 'foo.vtt',
174+
language: 'en',
175+
label: 'English'
176+
}, {
177+
kind: 'subtitles',
178+
src: 'bar.vtt',
179+
language: 'de',
180+
label: 'German'
181+
}]
182+
}, 'the object has the expected structure');
183+
});
184+
185+
// This only tests the relevant aspect of the reset function. The rest of its
186+
// effects are tested in player.test.js
187+
QUnit.test('reset discards the media object', function(assert) {
188+
this.player.loadMedia({
189+
poster: 'foo.jpg',
190+
src: 'foo.mp4',
191+
textTracks: [{src: 'foo.vtt'}]
192+
});
193+
194+
this.player.reset();
195+
196+
// TODO: There is a bug with player.reset() where it does not clear internal
197+
// cachces completely. Remove this when that's fixed.
198+
this.player.cache_.sources = [];
199+
200+
assert.deepEqual(this.player.getMedia(), {src: [], textTracks: []}, 'any empty media object is returned');
201+
});

0 commit comments

Comments
 (0)