diff --git a/package.json b/package.json index f3670db03..b5e514bb1 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "video.js": "^7 || ^8" }, "peerDependencies": { - "video.js": "^8.14.0" + "video.js": "^8.19.0" }, "devDependencies": { "@babel/cli": "^7.21.0", diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 1a6b75306..404b5927a 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -1079,7 +1079,19 @@ class VhsHandler extends Component { this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource); - this.tech_.src(this.mediaSourceUrl_); + // If we are playing HLS with MSE in Safari, add source elements for both the blob and manifest URLs. + // The latter will enable Airplay playback on receiver devices. + if (( + videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS) && + this.options_.overrideNative && + this.options_.sourceType === 'hls' && + typeof this.tech_.addSourceElement === 'function' + ) { + this.tech_.addSourceElement(this.mediaSourceUrl_); + this.tech_.addSourceElement(this.source_.src); + } else { + this.tech_.src(this.mediaSourceUrl_); + } } createKeySessions_() { diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index 929dc00b6..027ca75b1 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -3121,6 +3121,45 @@ QUnit.test( } ); +QUnit.test('uses source elements when overriding native HLS in Safari/iOS', function(assert) { + const origIsAnySafari = videojs.browser.IS_ANY_SAFARI; + const addSourceElementCalls = []; + let srcCalls = 0; + + videojs.browser.IS_ANY_SAFARI = true; + + const player = createPlayer({ html5: { vhs: { overrideNative: true } } }); + + player.tech_.addSourceElement = function(url) { + addSourceElementCalls.push(url); + }; + + player.tech_.src = function() { + srcCalls++; + }; + + player.src({ + src: 'http://example.com/manifest/main.m3u8', + type: 'application/x-mpegURL' + }); + + this.clock.tick(1); + + assert.equal(addSourceElementCalls.length, 2, '2 source elements added'); + assert.equal(srcCalls, 0, 'tech.src() not called'); + + const blobUrl = addSourceElementCalls[0]; + const manifestUrl = addSourceElementCalls[1]; + + assert.ok(blobUrl.startsWith('blob:'), 'First source element is a blob URL'); + assert.equal(manifestUrl, 'http://example.com/manifest/main.m3u8', 'Second source element is the manifest URL'); + + // Clean up and restore original flags + player.dispose(); + + videojs.browser.IS_ANY_SAFARI = origIsAnySafari; +}); + QUnit.test('re-emits mediachange events', function(assert) { let mediaChanges = 0;