From b4287f1826469a857f70ef916d8dffd0c359489b Mon Sep 17 00:00:00 2001 From: unclekingpin Date: Thu, 2 Nov 2023 14:21:16 -0700 Subject: [PATCH] update media capabilities detection --- package.json | 1 + src/mediaCapabilities.js | 189 +++++++++++------- .../withStreamingServer.js | 94 ++++----- 3 files changed, 160 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index 854033e..9f7dcc3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "hat": "0.0.3", "hls.js": "https://github.com/Stremio/hls.js/releases/download/v1.2.3-patch1/hls.js-1.2.3-patch1.tgz", "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.2", "magnet-uri": "6.2.0", "url": "0.11.0", "video-name-parser": "1.4.6", diff --git a/src/mediaCapabilities.js b/src/mediaCapabilities.js index a72e108..2c798f3 100644 --- a/src/mediaCapabilities.js +++ b/src/mediaCapabilities.js @@ -1,55 +1,99 @@ -var VIDEO_CODEC_CONFIGS = [ - { - codec: 'h264', - mime: 'video/mp4; codecs="avc1.42E01E"', - }, - { - codec: 'h265', - mime: 'video/mp4; codecs="hev1.1.6.L150.B0"', - aliases: ['hevc'] - }, - { - codec: 'vp8', - mime: 'video/mp4; codecs="vp8"' - }, - { - codec: 'vp9', - mime: 'video/mp4; codecs="vp9"' - } -]; +var MP4_CONFIG = { + VIDEO_CODECS: [ + { + codec: "h264", + force: window.chrome || window.cast, + mime: 'video/mp4; codecs="avc1.42E01E"', + }, + { + codec: "h265", + force: window.chrome || window.cast, + mime: 'video/mp4; codecs="hev1.1.6.L150.B0"', + aliases: ["hevc"], + }, + { + codec: "vp8", + mime: 'video/mp4; codecs="vp8"', + }, + { + codec: "vp9", + mime: 'video/mp4; codecs="vp9"', + }, + ], + AUDIO_CODEC: [ + { + codec: "aac", + force: window.chrome || window.cast, + mime: 'audio/mp4; codecs="mp4a.40.2"', + }, + { + codec: "mp3", + force: window.chrome || window.cast, + mime: 'audio/mp4; codecs="mp3"', + }, + { + codec: "ac3", + mime: 'audio/mp4; codecs="ac-3"', + }, + { + codec: "eac3", + mime: 'audio/mp4; codecs="ec-3"', + }, + { + codec: "vorbis", + mime: 'audio/mp4; codecs="vorbis"', + }, + { + codec: "opus", + mime: 'audio/mp4; codecs="opus"', + }, + ], +}; -var AUDIO_CODEC_CONFIGS = [ - { - codec: 'aac', - mime: 'audio/mp4; codecs="mp4a.40.2"' - }, - { - codec: 'mp3', - mime: 'audio/mp4; codecs="mp3"' - }, - { - codec: 'ac3', - mime: 'audio/mp4; codecs="ac-3"' - }, - { - codec: 'eac3', - mime: 'audio/mp4; codecs="ec-3"' - }, - { - codec: 'vorbis', - mime: 'audio/mp4; codecs="vorbis"' - }, - { - codec: 'opus', - mime: 'audio/mp4; codecs="opus"' - } -]; +var MATROSKA_CONFIG = { + VIDEO_CODECS: [ + { + codec: "h264", + force: window.chrome || window.cast, + }, + { + codec: "h265", + force: window.chrome || window.cast, + aliases: ["hevc"], + }, + { + codec: "vp8", + mime: 'video/webm; codecs="vp8"', + }, + { + codec: "vp9", + mime: 'video/webm; codecs="vp9"', + }, + ], + AUDIO_CODEC: [ + { + codec: "aac", + force: window.chrome || window.cast, + }, + { + codec: "mp3", + force: window.chrome || window.cast, + }, + { + codec: "vorbis", + mime: 'audio/webm; codecs="vorbis"', + }, + { + codec: "opus", + mime: 'audio/webm; codecs="opus"', + }, + ], +}; function canPlay(config, options) { - return options.mediaElement.canPlayType(config.mime) ? - [config.codec].concat(config.aliases || []) - : - []; + return config.force || options.mediaElement.canPlayType(config.mime) + ? [config.codec].concat(config.aliases || []) + : []; } function getMaxAudioChannels() { @@ -66,28 +110,35 @@ function getMaxAudioChannels() { } function getMediaCapabilities() { - var mediaElement = document.createElement('video'); - var formats = ['mp4', 'webm']; - var videoCodecs = VIDEO_CODEC_CONFIGS - .map(function(config) { - return canPlay(config, { mediaElement: mediaElement }); - }) - .reduce(function(result, value) { - return result.concat(value); - }, []); - var audioCodecs = AUDIO_CODEC_CONFIGS - .map(function(config) { - return canPlay(config, { mediaElement: mediaElement }); - }) - .reduce(function(result, value) { - return result.concat(value); - }, []); + var mediaElement = document.createElement("video"); var maxAudioChannels = getMaxAudioChannels(); return { - formats: formats, - videoCodecs: videoCodecs, - audioCodecs: audioCodecs, - maxAudioChannels: maxAudioChannels + mp4: { + videoCodecs: MP4_CONFIG.VIDEO_CODECS.map(function (config) { + return canPlay(config, { mediaElement: mediaElement }); + }).reduce(function (result, value) { + return result.concat(value); + }, []), + audioCodecs: MP4_CONFIG.AUDIO_CODEC.map(function (config) { + return canPlay(config, { mediaElement: mediaElement }); + }).reduce(function (result, value) { + return result.concat(value); + }, []), + maxAudioChannels, + }, + 'matroska,webm': { + videoCodecs: MATROSKA_CONFIG.VIDEO_CODECS.map(function (config) { + return canPlay(config, { mediaElement: mediaElement }); + }).reduce(function (result, value) { + return result.concat(value); + }, []), + audioCodecs: MATROSKA_CONFIG.AUDIO_CODEC.map(function (config) { + return canPlay(config, { mediaElement: mediaElement }); + }).reduce(function (result, value) { + return result.concat(value); + }, []), + maxAudioChannels, + }, }; } diff --git a/src/withStreamingServer/withStreamingServer.js b/src/withStreamingServer/withStreamingServer.js index 442e6e2..516c3bd 100644 --- a/src/withStreamingServer/withStreamingServer.js +++ b/src/withStreamingServer/withStreamingServer.js @@ -1,6 +1,7 @@ var EventEmitter = require('eventemitter3'); var url = require('url'); var hat = require('hat'); +var mergeWith = require('lodash.mergewith'); var cloneDeep = require('lodash.clonedeep'); var deepFreeze = require('deep-freeze'); var mediaCapabilities = require('../mediaCapabilities'); @@ -17,10 +18,10 @@ function withStreamingServer(Video) { video.on('propValue', onVideoPropEvent.bind(null, 'propValue')); video.on('propChanged', onVideoPropEvent.bind(null, 'propChanged')); Video.manifest.events - .filter(function(eventName) { + .filter(function (eventName) { return !['error', 'propValue', 'propChanged'].includes(eventName); }) - .forEach(function(eventName) { + .forEach(function (eventName) { video.on(eventName, onOtherVideoEvent(eventName)); }); @@ -52,7 +53,7 @@ function withStreamingServer(Video) { events.emit(eventName, propName, getProp(propName, propValue)); } function onOtherVideoEvent(eventName) { - return function() { + return function () { events.emit.apply(events, [eventName].concat(Array.from(arguments))); }; } @@ -103,38 +104,19 @@ function withStreamingServer(Video) { loadArgs = commandArgs; onPropChanged('stream'); convertStream(commandArgs.streamingServerURL, commandArgs.stream, commandArgs.seriesInfo) - .then(function(result) { + .then(function (result) { var mediaURL = result.url; var infoHash = result.infoHash; var fileIdx = result.fileIdx; - var formats = Array.isArray(commandArgs.formats) ? - commandArgs.formats - : - mediaCapabilities.formats; - var videoCodecs = Array.isArray(commandArgs.videoCodecs) ? - commandArgs.videoCodecs - : - mediaCapabilities.videoCodecs; - var audioCodecs = Array.isArray(commandArgs.audioCodecs) ? - commandArgs.audioCodecs - : - mediaCapabilities.audioCodecs; - var maxAudioChannels = commandArgs.maxAudioChannels !== null && isFinite(commandArgs.maxAudioChannels) ? - commandArgs.maxAudioChannels - : - mediaCapabilities.maxAudioChannels; var canPlayStreamOptions = Object.assign({}, commandArgs, { - formats: formats, - videoCodecs: videoCodecs, - audioCodecs: audioCodecs, - maxAudioChannels: maxAudioChannels + mediaCapabilities: mergeWith({}, mediaCapabilities, commandArgs.mediaCapabilities) }); return (commandArgs.forceTranscoding ? Promise.resolve(false) : VideoWithStreamingServer.canPlayStream({ url: mediaURL }, canPlayStreamOptions)) - .catch(function(error) { + .catch(function (error) { console.warn('Media probe error', error); return false; }) - .then(function(canPlay) { + .then(function (canPlay) { if (canPlay) { return { mediaURL: mediaURL, @@ -152,11 +134,11 @@ function withStreamingServer(Video) { queryParams.set('forceTranscoding', '1'); } - videoCodecs.forEach(function(videoCodec) { + videoCodecs.forEach(function (videoCodec) { queryParams.append('videoCodecs', videoCodec); }); - audioCodecs.forEach(function(audioCodec) { + audioCodecs.forEach(function (audioCodec) { queryParams.append('audioCodecs', audioCodec); }); @@ -169,7 +151,7 @@ function withStreamingServer(Video) { stream: { url: url.resolve(commandArgs.streamingServerURL, '/hlsv2/' + id + '/master.m3u8?' + queryParams.toString()), subtitles: Array.isArray(commandArgs.stream.subtitles) ? - commandArgs.stream.subtitles.map(function(track) { + commandArgs.stream.subtitles.map(function (track) { return Object.assign({}, track, { url: typeof track.url === 'string' ? url.resolve(commandArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString()) @@ -188,7 +170,7 @@ function withStreamingServer(Video) { }; }); }) - .then(function(result) { + .then(function (result) { if (commandArgs !== loadArgs) { return; } @@ -203,7 +185,7 @@ function withStreamingServer(Video) { loaded = true; flushActionsQueue(); fetchVideoParams(commandArgs.streamingServerURL, result.mediaURL, result.infoHash, result.fileIdx, commandArgs.stream.behaviorHints) - .then(function(result) { + .then(function (result) { if (commandArgs !== loadArgs) { return; } @@ -211,7 +193,7 @@ function withStreamingServer(Video) { videoParams = result; onPropChanged('videoParams'); }) - .catch(function(error) { + .catch(function (error) { if (commandArgs !== loadArgs) { return; } @@ -222,7 +204,7 @@ function withStreamingServer(Video) { onPropChanged('videoParams'); }); }) - .catch(function(error) { + .catch(function (error) { if (commandArgs !== loadArgs) { return; } @@ -251,7 +233,7 @@ function withStreamingServer(Video) { type: 'command', commandName: 'addExtraSubtitlesTracks', commandArgs: Object.assign({}, commandArgs, { - tracks: commandArgs.tracks.map(function(track) { + tracks: commandArgs.tracks.map(function (track) { return Object.assign({}, track, { url: typeof track.url === 'string' ? url.resolve(loadArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString()) @@ -304,14 +286,14 @@ function withStreamingServer(Video) { } } - this.on = function(eventName, listener) { + this.on = function (eventName, listener) { if (destroyed) { throw new Error('Video is destroyed'); } events.on(eventName, listener); }; - this.dispatch = function(action) { + this.dispatch = function (action) { if (destroyed) { throw new Error('Video is destroyed'); } @@ -340,39 +322,41 @@ function withStreamingServer(Video) { }; } - VideoWithStreamingServer.canPlayStream = function(stream, options) { + VideoWithStreamingServer.canPlayStream = function (stream, options) { return Video.canPlayStream(stream) - .then(function(canPlay) { + .then(function (canPlay) { if (!canPlay) { throw new Error('Fallback using /hlsv2/probe'); } return canPlay; }) - .catch(function() { + .catch(function () { var queryParams = new URLSearchParams([['mediaURL', stream.url]]); return fetch(url.resolve(options.streamingServerURL, '/hlsv2/probe?' + queryParams.toString())) - .then(function(resp) { + .then(function (resp) { return resp.json(); }) - .then(function(probe) { - var isFormatSupported = options.formats.some(function(format) { - return probe.format.name.indexOf(format) !== -1; - }); - var videoStreams = probe.streams.filter(function(stream) { + .then(function (probe) { + var format = options.mediaCapabilities[probe.format.name] + if (!format) { + return false; + } + + var videoStreams = probe.streams.filter(function (stream) { return stream.track === 'video'; }); - var areVideoStreamsSupported = videoStreams.length === 0 || videoStreams.some(function(stream) { - return options.videoCodecs.indexOf(stream.codec) !== -1; + var areVideoStreamsSupported = videoStreams.length === 0 || videoStreams.some(function (stream) { + return format.videoCodecs.indexOf(stream.codec) !== -1; }); - var audioStreams = probe.streams.filter(function(stream) { + var audioStreams = probe.streams.filter(function (stream) { return stream.track === 'audio'; }); - var areAudioStreamsSupported = audioStreams.length === 0 || audioStreams.some(function(stream) { - return stream.channels <= options.maxAudioChannels && - options.audioCodecs.indexOf(stream.codec) !== -1; + var areAudioStreamsSupported = audioStreams.length === 0 || audioStreams.some(function (stream) { + return stream.channels <= format.maxAudioChannels && + format.audioCodecs.indexOf(stream.codec) !== -1; }); - return isFormatSupported && areVideoStreamsSupported && areAudioStreamsSupported; + return areVideoStreamsSupported && areAudioStreamsSupported; }); }); }; @@ -381,11 +365,11 @@ function withStreamingServer(Video) { name: Video.manifest.name + 'WithStreamingServer', external: Video.manifest.external, props: Video.manifest.props.concat(['stream', 'videoParams']) - .filter(function(value, index, array) { return array.indexOf(value) === index; }), + .filter(function (value, index, array) { return array.indexOf(value) === index; }), commands: Video.manifest.commands.concat(['load', 'unload', 'destroy', 'addExtraSubtitlesTracks']) - .filter(function(value, index, array) { return array.indexOf(value) === index; }), + .filter(function (value, index, array) { return array.indexOf(value) === index; }), events: Video.manifest.events.concat(['propValue', 'propChanged', 'error']) - .filter(function(value, index, array) { return array.indexOf(value) === index; }) + .filter(function (value, index, array) { return array.indexOf(value) === index; }) }; return VideoWithStreamingServer;