From d314bf7275a18d427d197d450a78cf3da92b24fd Mon Sep 17 00:00:00 2001 From: Ronald Fiering Date: Tue, 7 Jul 2026 16:57:26 +0200 Subject: [PATCH] Force VP09 video codec --- src/config.js | 7 ++ src/player_api/yt-api.ts | 2 +- src/ui.js | 1 + src/userScript.ts | 1 + src/video-codec.ts | 191 +++++++++++++++++++++++++++++++++++++++ src/video-quality.ts | 6 +- 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/video-codec.ts diff --git a/src/config.js b/src/config.js index 43ea8551..8d43b63a 100644 --- a/src/config.js +++ b/src/config.js @@ -63,6 +63,13 @@ const configOptions = new Map([ desc: 'Force max resolution video playback' } ], + [ + 'forceVp9Codec', + { + default: true, + desc: 'Force VP09 video codec' + } + ], [ 'removeEndscreen', { diff --git a/src/player_api/yt-api.ts b/src/player_api/yt-api.ts index 704405aa..0350d71b 100644 --- a/src/player_api/yt-api.ts +++ b/src/player_api/yt-api.ts @@ -11,7 +11,7 @@ interface YTPlayerEventMap extends HTMLElementEventMap { onStateChange: PlayerState; } -interface VideoQualityData { +export interface VideoQualityData { formatId: string | undefined; qualityLabel: string; quality: string; diff --git a/src/ui.js b/src/ui.js index c7c34b1a..290c95ae 100644 --- a/src/ui.js +++ b/src/ui.js @@ -128,6 +128,7 @@ function createOptionsPanel() { elmContainer.appendChild(createConfigCheckbox('showWatch')); elmContainer.appendChild(createConfigCheckbox('removeShorts')); elmContainer.appendChild(createConfigCheckbox('forceHighResVideo')); + elmContainer.appendChild(createConfigCheckbox('forceVp9Codec')); elmContainer.appendChild(createConfigCheckbox('removeEndscreen')); elmContainer.appendChild(createConfigCheckbox('autoAccountSelect')); elmContainer.appendChild(createConfigCheckbox('enableSponsorBlock')); diff --git a/src/userScript.ts b/src/userScript.ts index 1edea6b7..8b7e873b 100644 --- a/src/userScript.ts +++ b/src/userScript.ts @@ -1,5 +1,6 @@ import 'whatwg-fetch'; import './domrect-polyfill'; +import './video-codec'; import { handleLaunch } from './utils'; diff --git a/src/video-codec.ts b/src/video-codec.ts new file mode 100644 index 00000000..5006c90e --- /dev/null +++ b/src/video-codec.ts @@ -0,0 +1,191 @@ +import { configRead } from './config'; +import { getPlayerManager, PlayerMode } from './player_api'; +import type { EventMapOf, PlayerManager, VideoQualityData } from './player_api'; + +const VP09_CODEC = 'vp09'; +const AV1_CODEC = 'av01'; +const VP9_FORMAT_IDS = new Set([ + '278', + '242', + '243', + '244', + '247', + '248', + '271', + '302', + '303', + '308', + '313', + '315', + '330', + '331', + '332', + '333', + '334', + '335', + '336', + '337' +]); + +interface StreamingFormat { + mimeType?: string; + [key: string]: unknown; +} + +interface StreamingData { + formats?: StreamingFormat[]; + adaptiveFormats?: StreamingFormat[]; + [key: string]: unknown; +} + +interface PlayerResponse { + streamingData?: StreamingData; + [key: string]: unknown; +} + +function shouldForce() { + return configRead('forceVp9Codec'); +} + +function isAv1MimeType(mimeType: string | undefined) { + return mimeType?.includes(AV1_CODEC) ?? false; +} + +function isVp09MimeType(mimeType: string | undefined) { + return mimeType?.includes(VP09_CODEC) ?? false; +} + +export function isVp9FormatId(formatId: string | undefined) { + return formatId !== undefined && VP9_FORMAT_IDS.has(formatId); +} + +export function getPreferredVp9FormatId( + qualityData: VideoQualityData[] | undefined +) { + return qualityData?.find((format) => { + return format.isPlayable && isVp9FormatId(format.formatId); + })?.formatId; +} + +function isVideoFormat(format: StreamingFormat) { + return format.mimeType?.startsWith('video/') ?? false; +} + +function isVp09Format(format: StreamingFormat) { + return isVp09MimeType(format.mimeType); +} + +function filterFormats(formats: StreamingFormat[] | undefined) { + if (!formats) return 0; + + const hasVp09Video = formats.some( + (format) => isVideoFormat(format) && isVp09Format(format) + ); + if (!hasVp09Video) return 0; + + const originalLength = formats.length; + formats.splice( + 0, + formats.length, + ...formats.filter( + (format) => !isVideoFormat(format) || isVp09Format(format) + ) + ); + + return originalLength - formats.length; +} + +function forceVp09(playerResponse: PlayerResponse) { + if (!shouldForce()) return; + + const streamingData = playerResponse.streamingData; + if (!streamingData) return; + + const removedFormats = filterFormats(streamingData.formats); + const removedAdaptiveFormats = filterFormats(streamingData.adaptiveFormats); + const removedTotal = removedFormats + removedAdaptiveFormats; + + if (removedTotal > 0) { + console.info( + `[video-codec] Filtered ${removedTotal} non-VP09 video formats` + ); + } +} + +const origParse = JSON.parse; +JSON.parse = function (text, reviver) { + const r = origParse(text, reviver); + + forceVp09(r); + + return r; +}; + +const originalCanPlayType = HTMLMediaElement.prototype.canPlayType; +HTMLMediaElement.prototype.canPlayType = function (mimeType) { + if (shouldForce()) { + if (isAv1MimeType(mimeType)) return ''; + if (isVp09MimeType(mimeType)) return 'probably'; + } + + return originalCanPlayType.call(this, mimeType); +}; + +const originalIsTypeSupported = window.MediaSource?.isTypeSupported; +if (originalIsTypeSupported) { + window.MediaSource.isTypeSupported = function (mimeType) { + if (shouldForce()) { + if (isAv1MimeType(mimeType)) return false; + if (isVp09MimeType(mimeType)) return true; + } + + return originalIsTypeSupported.call(this, mimeType); + }; +} + +const originalDecodingInfo = navigator.mediaCapabilities?.decodingInfo; +if (originalDecodingInfo) { + navigator.mediaCapabilities.decodingInfo = function (configuration) { + const mimeType = configuration.video?.contentType; + if (shouldForce() && isAv1MimeType(mimeType)) { + return Promise.resolve({ + supported: false, + smooth: false, + powerEfficient: false, + keySystemAccess: null + }); + } + + return originalDecodingInfo.call(this, configuration); + }; +} + +type EventMap = EventMapOf; + +function setVp09PlaybackFormat(this: PlayerManager, _: unknown) { + if (this.playerMode === PlayerMode.PREVIEW) return; + + this.removeEventListener('playbackStart', setVp09PlaybackFormat); + + const formatId = getPreferredVp9FormatId( + this.player.getAvailableQualityData() + ); + if (!formatId) { + console.warn('[video-codec] No playable VP09 formatId available'); + return; + } + + console.info(`[video-codec] Selecting VP09 formatId ${formatId}`); + this.player.setPlaybackQualityRange('highres', 'highres', formatId); +} + +function handleNewVideo(this: PlayerManager, _: EventMap['newVideo']) { + if (!shouldForce()) return; + + this.removeEventListener('playbackStart', setVp09PlaybackFormat); + this.addEventListener('playbackStart', setVp09PlaybackFormat); +} + +void getPlayerManager().then((playerManager) => { + playerManager.addEventListener('newVideo', handleNewVideo); +}); diff --git a/src/video-quality.ts b/src/video-quality.ts index 8c9b6bad..89a0d2d3 100644 --- a/src/video-quality.ts +++ b/src/video-quality.ts @@ -2,6 +2,7 @@ import { configRead } from './config'; import { getPlayerManager, PlayerMode } from './player_api'; import type { EventMapOf, PlayerManager, VideoID } from './player_api'; import { showNotification } from './ui'; +import { getPreferredVp9FormatId } from './video-codec'; const playerManager = await getPlayerManager(); @@ -35,7 +36,10 @@ function setPlaybackQuality(this: PlayerManager, _: unknown) { this.removeEventListener('playbackStart', setPlaybackQuality); const prevQuality = this.player.getPlaybackQualityLabel(); - this.player.setPlaybackQualityRange('highres', 'highres'); + const formatId = configRead('forceVp9Codec') + ? getPreferredVp9FormatId(this.player.getAvailableQualityData()) + : undefined; + this.player.setPlaybackQualityRange('highres', 'highres', formatId); if (prevQuality === getMaxQualityLabel(this.player)) { notifyPlaybackQuality.call(this);