1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-02 06:32:07 +02:00

feat: Add methods to add and remove <source> elements (#8886)

## Description
It is useful to have methods for appending and removing `<source>`
elements to the `<video>` element, as they are sometimes required to
enable certain playback features, for example, using [Airplay with
MSE](https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay).

## Specific Changes proposed
Add new methods-- `addSourceElement()` and `removeSourceElement()` to
the player and tech. The former will take a source object and create and
append a new `<source>` element to the `<video>` element, and the latter
will take a source url and remove any `<source>` element with a matching
`src`.

## Requirements Checklist
- [ ] Feature implemented / Bug fixed
- [ ] If necessary, more likely in a feature request than a bug fix
- [ ] Change has been verified in an actual browser (Chrome, Firefox,
IE)
  - [ ] Unit Tests updated or fixed
  - [ ] Docs/guides updated
- [ ] Example created ([starter template on
JSBin](https://codepen.io/gkatsev/pen/GwZegv?editors=1000#0))
- [ ] Has no DOM changes which impact accessiblilty or trigger warnings
(e.g. Chrome issues tab)
  - [ ] Has no changes to JSDoc which cause `npm run docs:api` to error
- [ ] Reviewed by Two Core Contributors
This commit is contained in:
Alex Barstow 2024-10-09 12:16:04 -04:00 committed by GitHub
parent 077077b00a
commit eddda97eeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 230 additions and 0 deletions

View File

@ -3710,6 +3710,43 @@ class Player extends Component {
return false; return false;
} }
/**
* Add a <source> element to the <video> element.
*
* @param {string} srcUrl
* The URL of the video source.
*
* @param {string} [mimeType]
* The MIME type of the video source. Optional but recommended.
*
* @return {boolean}
* Returns true if the source element was successfully added, false otherwise.
*/
addSourceElement(srcUrl, mimeType) {
if (!this.tech_) {
return false;
}
return this.tech_.addSourceElement(srcUrl, mimeType);
}
/**
* Remove a <source> element from the <video> element by its URL.
*
* @param {string} srcUrl
* The URL of the source to remove.
*
* @return {boolean}
* Returns true if the source element was successfully removed, false otherwise.
*/
removeSourceElement(srcUrl) {
if (!this.tech_) {
return false;
}
return this.tech_.removeSourceElement(srcUrl);
}
/** /**
* Begin loading the src data. * Begin loading the src data.
*/ */

View File

@ -783,6 +783,67 @@ class Html5 extends Tech {
this.setSrc(src); this.setSrc(src);
} }
/**
* Add a <source> element to the <video> element.
*
* @param {string} srcUrl
* The URL of the video source.
*
* @param {string} [mimeType]
* The MIME type of the video source. Optional but recommended.
*
* @return {boolean}
* Returns true if the source element was successfully added, false otherwise.
*/
addSourceElement(srcUrl, mimeType) {
if (!srcUrl) {
log.error('Invalid source URL.');
return false;
}
const sourceAttributes = { src: srcUrl };
if (mimeType) {
sourceAttributes.type = mimeType;
}
const sourceElement = Dom.createEl('source', {}, sourceAttributes);
this.el_.appendChild(sourceElement);
return true;
}
/**
* Remove a <source> element from the <video> element by its URL.
*
* @param {string} srcUrl
* The URL of the source to remove.
*
* @return {boolean}
* Returns true if the source element was successfully removed, false otherwise.
*/
removeSourceElement(srcUrl) {
if (!srcUrl) {
log.error('Source URL is required to remove the source element.');
return false;
}
const sourceElements = this.el_.querySelectorAll('source');
for (const sourceElement of sourceElements) {
if (sourceElement.src === srcUrl) {
this.el_.removeChild(sourceElement);
return true;
}
}
log.warn(`No matching source element found with src: ${srcUrl}`);
return false;
}
/** /**
* Reset the tech by removing all sources and then calling * Reset the tech by removing all sources and then calling
* {@link Html5.resetMediaElement}. * {@link Html5.resetMediaElement}.

View File

@ -3611,3 +3611,57 @@ QUnit.test('smooth seeking set to true should update the display time components
player.dispose(); player.dispose();
}); });
QUnit.test('addSourceElement calls tech method with correct args', function(assert) {
const player = TestHelpers.makePlayer();
const addSourceElementSpy = sinon.spy(player.tech_, 'addSourceElement');
const srcUrl = 'http://example.com/video.mp4';
const mimeType = 'video/mp4';
player.addSourceElement(srcUrl, mimeType);
assert.ok(addSourceElementSpy.calledOnce, 'addSourceElement method called');
assert.ok(addSourceElementSpy.calledWith(srcUrl, mimeType), 'addSourceElement called with correct arguments');
addSourceElementSpy.restore();
player.dispose();
});
QUnit.test('addSourceElement returns false if no tech', function(assert) {
const player = TestHelpers.makePlayer();
const srcUrl = 'http://example.com/video.mp4';
const mimeType = 'video/mp4';
player.tech_ = undefined;
const added = player.addSourceElement(srcUrl, mimeType);
assert.notOk(added, 'Returned false');
player.dispose();
});
QUnit.test('removeSourceElement calls tech method with correct args', function(assert) {
const player = TestHelpers.makePlayer();
const removeSourceElementSpy = sinon.spy(player.tech_, 'removeSourceElement');
const srcUrl = 'http://example.com/video.mp4';
player.removeSourceElement(srcUrl);
assert.ok(removeSourceElementSpy.calledOnce, 'removeSourceElement method called');
assert.ok(removeSourceElementSpy.calledWith(srcUrl), 'removeSourceElement called with correct arguments');
removeSourceElementSpy.restore();
player.dispose();
});
QUnit.test('removeSourceElement returns false if no tech', function(assert) {
const player = TestHelpers.makePlayer();
const srcUrl = 'http://example.com/video.mp4';
const mimeType = 'video/mp4';
player.tech_ = undefined;
const removed = player.removeSourceElement(srcUrl, mimeType);
assert.notOk(removed, 'Returned false');
player.dispose();
});

View File

@ -905,3 +905,79 @@ QUnit.test('supportsFullScreen is always with `webkitEnterFullScreen`', function
tech.el_ = oldEl; tech.el_ = oldEl;
}); });
QUnit.test('addSourceElement adds a valid source element with srcUrl and mimeType', function(assert) {
const videoEl = document.createElement('video');
tech.el_ = videoEl;
const srcUrl = 'http://example.com/video.mp4';
const mimeType = 'video/mp4';
const added = tech.addSourceElement(srcUrl, mimeType);
const sourceElement = videoEl.querySelector('source');
assert.ok(added, 'Returned true');
assert.ok(sourceElement, 'A source element was added');
assert.equal(sourceElement.src, srcUrl, 'Source element has correct src');
assert.equal(sourceElement.type, mimeType, 'Source element has correct type');
});
QUnit.test('addSourceElement adds a valid source element without a mimeType', function(assert) {
const videoEl = document.createElement('video');
tech.el_ = videoEl;
const srcUrl = 'http://example.com/video2.mp4';
const added = tech.addSourceElement(srcUrl);
const sourceElement = videoEl.querySelector('source');
assert.ok(added, 'Returned true');
assert.ok(sourceElement, 'A source element was added even without a type');
assert.equal(sourceElement.src, srcUrl, 'Source element has correct src');
assert.notOk(sourceElement.type, 'Source element does not have a type attribute');
});
QUnit.test('addSourceElement does not add a source element for invalid source URL', function(assert) {
const videoEl = document.createElement('video');
tech.el_ = videoEl;
const added = tech.addSourceElement('');
const sourceElement = videoEl.querySelector('source');
assert.notOk(added, 'Returned false');
assert.notOk(sourceElement, 'No source element was added for missing src');
});
QUnit.test('removeSourceElement removes a source element by its URL', function(assert) {
const videoEl = document.createElement('video');
tech.el_ = videoEl;
tech.addSourceElement('http://example.com/video1.mp4');
tech.addSourceElement('http://example.com/video2.mp4');
let sourceElement = videoEl.querySelector('source[src="http://example.com/video1.mp4"]');
assert.ok(sourceElement, 'Source element for video1.mp4 was added');
const removed = tech.removeSourceElement('http://example.com/video1.mp4');
assert.ok(removed, 'Source element was successfully removed');
sourceElement = videoEl.querySelector('source[src="http://example.com/video1.mp4"]');
assert.notOk(sourceElement, 'Source element for video1.mp4 was removed');
});
QUnit.test('removeSourceElement does not remove a source element if URL does not match', function(assert) {
const videoEl = document.createElement('video');
tech.el_ = videoEl;
tech.addSourceElement('http://example.com/video.mp4');
const removed = tech.removeSourceElement('http://example.com/invalid.mp4');
assert.notOk(removed, 'No source element was removed for non-matching URL');
});

View File

@ -111,6 +111,8 @@ class TechFaker extends Tech {
} }
return 'movie.mp4'; return 'movie.mp4';
} }
addSourceElement() {}
removeSourceElement() {}
load() { load() {
} }
currentSrc() { currentSrc() {