From 9f3fb46d371d52f58bc9a7fc5beefe51890879ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Thu, 13 Jan 2022 23:07:35 +0100 Subject: [PATCH] feat: Allow WebP and AVIF image streams (#3856) Fixes: #3845 --- lib/util/stream_utils.js | 101 ++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index ca32535840..3ed813b9a6 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -406,7 +406,7 @@ shaka.util.StreamUtils = class { shaka.util.StreamUtils.filterManifestByCurrentVariant( currentVariant, manifest); shaka.util.StreamUtils.filterTextStreams_(manifest); - shaka.util.StreamUtils.filterImageStreams_(manifest); + await shaka.util.StreamUtils.filterImageStreams_(manifest); } @@ -761,37 +761,57 @@ shaka.util.StreamUtils = class { * @param {shaka.extern.Manifest} manifest * @private */ - static filterImageStreams_(manifest) { - // Filter image streams. - manifest.imageStreams = manifest.imageStreams.filter((stream) => { - // TODO: re-examine this and avoid allow-listing the MIME types we can - // accept. - const validMimeTypes = [ - 'image/svg+xml', - 'image/png', - 'image/jpeg', - ]; - const Platform = shaka.util.Platform; - // Add webp support to popular platforms that support it. - const webpSupport = Platform.isWebOS() || - Platform.isTizen() || - Platform.isChromecast(); - if (webpSupport) { - validMimeTypes.push('image/webp'); + static async filterImageStreams_(manifest) { + const imageStreams = []; + for (const stream of manifest.imageStreams) { + const mimeType = stream.mimeType; + if (!shaka.util.StreamUtils.supportedImageMimeTypes_.has(mimeType)) { + const minImage = shaka.util.StreamUtils.minImage_.get(mimeType); + if (minImage) { + // eslint-disable-next-line no-await-in-loop + const res = await shaka.util.StreamUtils.isImageSupported_(minImage); + shaka.util.StreamUtils.supportedImageMimeTypes_.set(mimeType, res); + } else { + shaka.util.StreamUtils.supportedImageMimeTypes_.set(mimeType, false); + } } - // TODO: add support to image/webp and image/avif - const keep = validMimeTypes.includes(stream.mimeType); + + const keep = + shaka.util.StreamUtils.supportedImageMimeTypes_.get(mimeType); if (!keep) { shaka.log.debug('Dropping image stream. Is not supported by the ' + 'platform.', stream); + } else { + imageStreams.push(stream); } + } + manifest.imageStreams = imageStreams; + } - return keep; + /** + * @param {string} minImage + * @return {!Promise.} + * @private + */ + static isImageSupported_(minImage) { + return new Promise((resolve) => { + const imageElement = /** @type {HTMLImageElement} */(new Image()); + imageElement.src = minImage; + if ('decode' in imageElement) { + imageElement.decode().then(() => { + resolve(true); + }).catch(() => { + resolve(false); + }); + } else { + imageElement.onload = imageElement.onerror = () => { + resolve(imageElement.height === 2); + }; + } }); } - /** * @param {shaka.extern.Stream} s0 * @param {shaka.extern.Stream} s1 @@ -1445,3 +1465,40 @@ shaka.util.StreamUtils.DecodingAttributes = { POWER: 'powerEfficient', BANDWIDTH: 'bandwidth', }; + +/** + * @private {!Map.} + */ +shaka.util.StreamUtils.supportedImageMimeTypes_ = new Map() + .set('image/svg+xml', true) + .set('image/png', true) + .set('image/jpeg', true) + .set('image/jpg', true); + +/** + * @const {string} + * @private + */ +shaka.util.StreamUtils.minWebPImage_ = 'data:image/webp;base64,UklGRjoAAABXRU' + + 'JQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwY' + + 'AAA'; + +/** + * @const {string} + * @private + */ +shaka.util.StreamUtils.minAvifImage_ = 'data:image/avif;base64,AAAAIGZ0eXBhdm' + + 'lmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljd' + + 'AAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEA' + + 'AAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAA' + + 'AamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAA' + + 'xhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAA' + + 'CVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A='; + +/** + * @const {!Map.} + * @private + */ +shaka.util.StreamUtils.minImage_ = new Map() + .set('image/webp', shaka.util.StreamUtils.minWebPImage_) + .set('image/avif', shaka.util.StreamUtils.minAvifImage_);