├── .gitignore ├── .jpmignore ├── README.md ├── data ├── context-menu.js ├── html5-play.png ├── settings.svg ├── youtube-html5.css ├── youtube-html5.js └── youtube-video-quality.js ├── lib ├── CookieChanger.js ├── PluginPermissionChanger.js ├── URIChanger.js ├── UserAgentChanger.js └── main.js ├── logo.png ├── logo.svg ├── package.json └── test └── test-cookies.js /.gitignore: -------------------------------------------------------------------------------- 1 | releases -------------------------------------------------------------------------------- /.jpmignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.gitignore 3 | /.jpmignore 4 | /README.md 5 | /logo.svg 6 | /releases 7 | /*.xpi 8 | /test/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube ALL HTML5 2 | 3 | Play all videos on YouTube with your preferred settings (size, quality, 4 | playback rate, …) without cookies using only HTML5. 5 | 6 | * Automatically change the size of the player and the resolution of the video 7 | * Force HTML5 in cases when YouTube still defaults to Flash 8 | * Start videos paused (always, when not in a playlist, when in a background tab) 9 | * Open video links without the attached playlist (context menu entry) 10 | * Features for users that don't keep cookies: 11 | * Automatically set volume and playback rate 12 | * Disable autoplay of recommended videos 13 | * Hide video annotations 14 | 15 | All features of the add-on can be individually configured in the settings 16 | (`about:addons`). These are also directly accessible through a button on every 17 | YouTube video page. 18 | 19 | ## Build 20 | 21 | The add-on can be built using `jpm`: 22 | 23 | ```sh 24 | jpm xpi 25 | ``` 26 | 27 | This creates an unsigned add-on. To build a signed version, you have to change 28 | the add-on id and submit the add-on to [Mozilla Add-ons][amo] for signing. 29 | 30 | ## Develop 31 | 32 | You can run the add-on directly in Firefox with a fresh profile and get log 33 | messages on your terminal: 34 | 35 | ```sh 36 | jpm -b firefox-dev run 37 | ``` 38 | 39 | Currently `firefox-dev` has to be an aurora (dev) or nightly build of Firefox, 40 | because of a [bug in `jpm`][jpm-468]. 41 | 42 | ## Use 43 | 44 | You need a current version of Firefox with support for MSE and VP9 or H264. You 45 | may have to install `ffmpeg` on Linux. Without MSE, only 360p and 720p videos 46 | are available. 47 | 48 | YouTube provides a [test site] that checks support for the various pieces 49 | necessary. If some element is missing, check the following Firefox settings: 50 | 51 | * `media.mediasource.enabled` 52 | * `media.mediasource.webm.enabled` 53 | * `media.mediasource.mp4.enabled` 54 | 55 | ## Attribution 56 | 57 | Thanks to [Raylan Givens][rg] for the hint at emulating IE, 58 | [Alexander Schlarb][as] for his patch which makes the add-on work with 59 | Firefox's click_to_play, [Alex Szczuczko][aszc] for implementing the "pause on 60 | start" function and [timendum][timendum] for his work on embedded videos. 61 | 62 | Also thanks to [Jeppe Rune Mortensen][YePpHa] and [Yonezpt] for their work on 63 | [YouTubeCenter][ytc]. 64 | 65 | ## Licence 66 | 67 | This add-on by Klemens Schölhorn is licenced under GPLv3.
68 | The modified [HTML5 Logo][w3c] by W3C is licenced under CC-BY 3.0. 69 | 70 | [w3c]: http://www.w3.org/html/logo/ 71 | [rg]: https://addons.mozilla.org/de/firefox/user/Cullen-Bohannon/ 72 | [as]: https://github.com/alexander255 73 | [aszc]: https://github.com/ASzc 74 | [timendum]: https://github.com/timendum 75 | [YePpHa]: https://github.com/YePpHa 76 | [Yonezpt]: https://github.com/Yonezpt 77 | [ytc]: https://github.com/YePpHa/YouTubeCenter 78 | [jpm-468]: https://github.com/mozilla-jetpack/jpm/issues/468 79 | [amo]: https://addons.mozilla.org/ 80 | [test site]: https://www.youtube.com/html5 81 | -------------------------------------------------------------------------------- /data/context-menu.js: -------------------------------------------------------------------------------- 1 | // http://www.techtricky.com/how-to-get-url-parameters-using-javascript/ 2 | function parseUrlQuery(query) { 3 | var params = {}; 4 | query.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(str, key, value) { 5 | params[key] = value; 6 | }); 7 | return params; 8 | } 9 | 10 | self.on("click", function(node, data) { 11 | self.postMessage(parseUrlQuery(node.search).v); 12 | }); 13 | 14 | self.on("context", function(link) { 15 | // link could be any descendant of the a-tag 16 | while(!(link instanceof HTMLAnchorElement)) { 17 | if(link.parentElement !== null) { 18 | link = link.parentElement; 19 | } else { 20 | // should not happen, as this function is 21 | // only called for clicks on "a[href]" 22 | return false; 23 | } 24 | } 25 | 26 | // link points to a youtube video 27 | if(link.hostname != "www.youtube.com" || 28 | link.pathname != "/watch") { 29 | return false; 30 | } 31 | 32 | var parameters = parseUrlQuery(link.search); 33 | 34 | // check that the video link is not broken 35 | if(!("v" in parameters)) { 36 | return false; 37 | } 38 | 39 | // show the entry for video links which include a playlist 40 | return ("list" in parameters); 41 | }); 42 | -------------------------------------------------------------------------------- /data/html5-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klemens/ff-youtube-all-html5/2f8faa4584b8691af64a5ac2f79fe2057cc03fb8/data/html5-play.png -------------------------------------------------------------------------------- /data/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 22 | 23 | image/svg+xmlOpenclipart -------------------------------------------------------------------------------- /data/youtube-html5.css: -------------------------------------------------------------------------------- 1 | /* Remove red "flash not found" alert bar */ 2 | .yt-alert.yt-alert-default.yt-alert-error.yt-alert-player { 3 | display: none !important; 4 | } 5 | -------------------------------------------------------------------------------- /data/youtube-html5.js: -------------------------------------------------------------------------------- 1 | function youtubeHtml5ButtonLoader(startOptions) { 2 | var options = startOptions; 3 | 4 | var html5Button = null; 5 | 6 | var that = this; 7 | 8 | this.installButton = function() { 9 | var insertInto = document.getElementById("yt-masthead-user") 10 | || document.getElementById("yt-masthead-signin") 11 | || document.getElementById("yt-masthead-content"); 12 | if(!insertInto) { 13 | return; 14 | } 15 | 16 | // create the html5 button 17 | html5Button = document.createElement("button"); 18 | html5Button.className = "yt-uix-button yt-uix-button-default yt-uix-button-size-default yt-uix-button-empty yt-uix-tooltip"; 19 | html5Button.dataset.tooltipText = "YouTube ALL HTML5 - " + options.version; 20 | html5Button.style.backgroundImage = "url(" + options.buttonImageUrl + ")"; 21 | html5Button.style.backgroundRepeat = "no-repeat"; 22 | html5Button.style.backgroundPosition = "5px 50%"; 23 | html5Button.style.padding = "0 5px 0 30px"; 24 | html5Button.style.marginRight = "15px"; 25 | 26 | var arrowImage = document.createElement("img"); 27 | arrowImage.className = "yt-uix-button-arrow"; 28 | arrowImage.src = "//s.ytimg.com/yts/img/pixel-vfl3z5WfW.gif"; 29 | html5Button.appendChild(arrowImage); 30 | 31 | var menuList = document.createElement("ol"); 32 | menuList.className = "yt-uix-button-menu hid"; 33 | html5Button.appendChild(menuList); 34 | 35 | // only add the manual size controls to the menu when we are also 36 | // resizing the player automatically, because they depend on deleting 37 | // the window.matchMedia function before the player is even loaded 38 | // (see the video quality content script for details) 39 | var height = parseInt(options.settings["yt-player-height"]); 40 | if(height > 0 || height == -1) { 41 | for(var i in options.playerHeights) { 42 | var li = document.createElement("li"); 43 | menuList.appendChild(li); 44 | 45 | var span = document.createElement("span"); 46 | span.className = "yt-uix-button-menu-item"; 47 | span.style.padding = "0 1em"; 48 | span.textContent = "Resize to " + options.playerHeights[i] + "p"; 49 | span.dataset.playersize = options.playerHeights[i]; 50 | span.addEventListener("click", function(event) { 51 | resizePlayer(event.target.dataset.playersize); 52 | }); 53 | li.appendChild(span); 54 | } 55 | } 56 | 57 | // add force playback link 58 | var li = document.createElement("li"); 59 | menuList.appendChild(li); 60 | var span = document.createElement("span"); 61 | span.className = "yt-uix-button-menu-item"; 62 | span.style.padding = "0 1em"; 63 | span.textContent = "Force playback"; 64 | span.addEventListener("click", function() { 65 | that.startVideo(); 66 | }); 67 | li.appendChild(span); 68 | 69 | // add settings link 70 | li = document.createElement("li"); 71 | menuList.appendChild(li); 72 | span = document.createElement("span"); 73 | span.className = "yt-uix-button-menu-item"; 74 | span.style.padding = "0 1em 0 2.8em"; 75 | span.style.backgroundImage = "url(" + options.settingsImageUrl + ")"; 76 | span.style.backgroundRepeat = "no-repeat"; 77 | span.style.backgroundSize = "14px"; 78 | span.style.backgroundPosition = "1em 5px"; 79 | span.textContent = "Settings"; 80 | span.addEventListener("click", function(event) { 81 | self.port.emit("openSettings", ""); 82 | }); 83 | li.appendChild(span); 84 | 85 | // insert into dom 86 | insertInto.insertBefore(html5Button, insertInto.firstChild); 87 | } 88 | 89 | this.showButton = function() { 90 | if(html5Button) { 91 | html5Button.parentNode.style.removeProperty("display"); 92 | } else { 93 | that.installButton(); 94 | } 95 | } 96 | 97 | this.hideButton = function() { 98 | if(html5Button) { 99 | html5Button.parentNode.style.setProperty("display", "none"); 100 | } 101 | } 102 | 103 | this.isVideoSite = function() { 104 | return /\/watch.*/.test(window.location.pathname); 105 | } 106 | 107 | this.isPlaylistSite = function() { 108 | return !! (getUrlParams().list); 109 | } 110 | 111 | this.startVideo = function() { 112 | var url = getUrlParams(); 113 | 114 | if(url && url.v) { 115 | var insertInto = document.getElementById("player-api-legacy") || 116 | document.getElementById("player-api"); 117 | insertVideoIframe(url.v, insertInto); 118 | 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | 125 | this.hideFlashPlugin = function() { 126 | // By Alexander Schlarb, alexander4456@xmine128.tk 127 | var unsafeNavigator = window.navigator.wrappedJSObject; 128 | 129 | // Generate new plugins list 130 | var plugins = []; 131 | for(var i = 0; i < unsafeNavigator.plugins.length; ++i) { 132 | var plugin = unsafeNavigator.plugins[i]; 133 | 134 | if(plugin.name != "Shockwave Flash") { 135 | plugins.push(plugin); 136 | } 137 | } 138 | 139 | // Use fake plugins list overwrite 140 | unsafeNavigator.__defineGetter__("plugins", function() { 141 | return plugins; 142 | }); 143 | 144 | // Generate new MIME types list 145 | var mimeTypes = []; 146 | for(var i = 0; i < unsafeNavigator.mimeTypes.length; ++i) { 147 | var mimeType = unsafeNavigator.mimeTypes[i]; 148 | 149 | if(mimeType.type != "application/x-shockwave-flash") { 150 | mimeTypes.push(mimeType); 151 | } 152 | } 153 | 154 | // Register fake mime types list overwrite 155 | unsafeNavigator.__defineGetter__("mimeTypes", function() { 156 | return mimeTypes; 157 | }); 158 | } 159 | 160 | this.autoSizeVideo = function() { 161 | resizePlayer(options.settings["yt-player-height"]); 162 | } 163 | 164 | this.autoHideAnnotations = function() { 165 | if(options.settings["yt-hide-annotations"]) { 166 | for(div of document.querySelectorAll(".video-annotations")) { 167 | div.style.display = "none"; 168 | } 169 | } 170 | } 171 | 172 | 173 | function resizePlayer(height) { 174 | var playerApi = document.getElementById("player-api-legacy") || 175 | document.getElementById("player-api"); 176 | var player = document.getElementById("player-legacy") || 177 | document.getElementById("player"); 178 | 179 | height = parseInt(height); 180 | 181 | if(height == 0 || height == -2 || !playerApi || !player) return; 182 | 183 | // this differs between youtube designs, known: 225px, 0px (new) 184 | var leftPadding = parseInt(window.getComputedStyle(player). 185 | getPropertyValue('padding-left')); 186 | 187 | // try to calculate the heigt based on site width 188 | if(height < 0) { 189 | var availableWidth = document.body.clientWidth - leftPadding; 190 | 191 | var sizesReverse = options.playerHeights.slice().reverse(); 192 | for(var i in sizesReverse) { 193 | if(availableWidth >= (sizesReverse[i] * 16 / 9)) { 194 | height = sizesReverse[i]; 195 | break; 196 | } 197 | } 198 | 199 | if(height < 0) return; 200 | } 201 | 202 | // Sidebar has negative top margin by default 203 | var sidebar = document.getElementById("watch7-sidebar"); 204 | if(sidebar) { 205 | sidebar.style.transition = "none"; 206 | sidebar.style.marginTop = "0"; 207 | sidebar.style.top = "0"; 208 | } 209 | 210 | // Fix playlist position 211 | var playlist = document.getElementById("watch-appbar-playlist"); 212 | if(playlist) { 213 | playlist.style.setProperty("margin-left", "0", "important"); 214 | playlist.style.marginTop = "10px"; 215 | } 216 | 217 | var placeholderPlayer = document.getElementById("placeholder-player"); 218 | if(placeholderPlayer) { 219 | placeholderPlayer.style.display = "none"; 220 | } 221 | 222 | player.style.width = (height * 16 / 9) + "px"; 223 | player.style.marginBottom = "10px"; 224 | player.style.maxWidth = "none"; 225 | playerApi.style.position = "relative"; 226 | playerApi.style.margin = "auto"; 227 | playerApi.style.left = "auto"; 228 | playerApi.style.width = (height * 16 / 9) + "px"; 229 | playerApi.style.height = (height + 30) + "px"; // 30px for nav 230 | 231 | // fixes for the new player design 232 | var chromeBottom = document.querySelector(".ytp-chrome-bottom"); 233 | if(chromeBottom) { 234 | // new desing uses floating controls, so remove extra 30 px 235 | playerApi.style.height = height + "px"; 236 | 237 | var sidebarSpacer = document.getElementById("watch-sidebar-spacer"); 238 | if(sidebarSpacer) { 239 | sidebarSpacer.style.display = "none"; 240 | } 241 | } 242 | } 243 | 244 | function insertVideoIframe(video, insertInto) { 245 | if(!insertInto) { 246 | return; 247 | } 248 | 249 | var player = document.createElement("iframe"); 250 | 251 | player.src = location.protocol + "//www.youtube.com/embed/" + video + "?rel=0&autoplay=1"; 252 | player.id = "fallbackIframe"; 253 | player.width = "100%"; 254 | player.height = "100%"; 255 | player.setAttribute('allowfullscreen', ''); 256 | 257 | // Remove all childern before inserting iframe 258 | while(insertInto.hasChildNodes()) { 259 | insertInto.removeChild(insertInto.firstChild); 260 | } 261 | insertInto.appendChild(player); 262 | } 263 | 264 | // http://www.techtricky.com/how-to-get-url-parameters-using-javascript/ 265 | function getUrlParams() { 266 | var params = {}; 267 | window.location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(str, key, value) { 268 | params[key] = value; 269 | }); 270 | return params; 271 | } 272 | } 273 | 274 | function isPolymer() { 275 | return document.getElementById("polymer-app") !== null; 276 | } 277 | 278 | var youtubeHtml5Button = new youtubeHtml5ButtonLoader(self.options); 279 | 280 | // remove flash plugin from the supported plugin list 281 | // this makes youtube think flash is disabled when click_to_play is enabled 282 | if(!(window.wrappedJSObject.ytplayer && window.wrappedJSObject.ytplayer.config && 283 | window.wrappedJSObject.ytplayer.config.args["live_playback"])) { 284 | youtubeHtml5Button.hideFlashPlugin(); 285 | } 286 | 287 | // install button if we are on a video site 288 | if(!isPolymer() && youtubeHtml5Button.isVideoSite()) { 289 | youtubeHtml5Button.installButton(); 290 | youtubeHtml5Button.autoSizeVideo(); 291 | youtubeHtml5Button.autoHideAnnotations(); 292 | } 293 | 294 | // check if spf is enabled 295 | if(!isPolymer() && window.wrappedJSObject.ytspf && window.wrappedJSObject.ytspf.enabled) { 296 | if(self.options.settings["yt-disable-spf"]) { 297 | // disbale spf by disposing the spf object 298 | // inspired by YePpHa's YouTubeCenter (https://github.com/YePpHa/YouTubeCenter) 299 | if(window.wrappedJSObject.spf && window.wrappedJSObject.spf.dispose) { 300 | window.wrappedJSObject.spf.dispose(); 301 | } 302 | } else { 303 | // listen for spf page changes, update button (and start video) 304 | var spfObserver = new MutationObserver(function(mutations) { 305 | mutations.forEach(function(mutation) { 306 | for(var i = 0; i < mutation.removedNodes.length; ++i) { 307 | if(mutation.removedNodes[i].id && mutation.removedNodes[i].id == "progress") { 308 | if(!isPolymer() && youtubeHtml5Button.isVideoSite()) { 309 | youtubeHtml5Button.showButton(); 310 | youtubeHtml5Button.autoSizeVideo(); 311 | youtubeHtml5Button.autoHideAnnotations(); 312 | } else { 313 | youtubeHtml5Button.hideButton(); 314 | } 315 | 316 | return; 317 | } 318 | } 319 | }); 320 | }); 321 | spfObserver.observe(document.body, { childList: true }); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /data/youtube-video-quality.js: -------------------------------------------------------------------------------- 1 | // Object that contains all functions and data that must be available to the page 2 | var _ytallhtml5 = createObjectIn(unsafeWindow, {defineAs: "_ytallhtml5"}); 3 | 4 | // Make config options available 5 | _ytallhtml5.options = cloneInto(self.options, unsafeWindow); 6 | 7 | runInPageContext(() => { 8 | // We only need to fix the video and controls sizes if we are changing the player size 9 | var height = parseInt(_ytallhtml5.options.settings["yt-player-height"]); 10 | if(height == 0 || height == -2) return; 11 | 12 | // Deleting the matchMedia method prevents the player in the old design 13 | // from querying the page size, which makes if fall back to the size the 14 | // page (or add-ons like us) specified for the containing div. 15 | // This does not work for the new polymer design, so we have to adjust 16 | // dynamically (returning undefined while polymer is still loading seems 17 | // unproblematic). 18 | window._matchMedia = window.matchMedia; 19 | delete window.matchMedia; 20 | Object.defineProperty(window, "matchMedia", { 21 | get: function() { 22 | if(document.getElementById("polymer-app") !== null) { 23 | return window._matchMedia; 24 | } else { 25 | return undefined; 26 | } 27 | } 28 | }); 29 | }); 30 | 31 | runInPageContext(() => { 32 | // Hijack the youtube config variable so we can modify it instanly upon setting 33 | delete window.ytplayer; 34 | window._ytplayer = {}; 35 | Object.defineProperty(window, "ytplayer", { 36 | get: function() { 37 | var resolution = null; 38 | if(_ytallhtml5.options.settings["yt-video-resolution"] === "auto") { 39 | if(_ytallhtml5.options.settings["yt-player-height"] !== 0) { 40 | resolution = _ytallhtml5.findBestResolution(_ytallhtml5.options.settings["yt-player-height"]); 41 | } 42 | } else { 43 | resolution = _ytallhtml5.options.settings["yt-video-resolution"]; 44 | } 45 | if(resolution) { 46 | _ytplayer.config = _ytplayer.config || {}; 47 | _ytplayer.config.args = _ytplayer.config.args || {}; 48 | 49 | _ytplayer.config.args.video_container_override = resolution; 50 | // suggestedQuality may not work anymore 51 | _ytplayer.config.args.suggestedQuality = _ytallhtml5.resolutionToYTQuality(resolution); 52 | _ytplayer.config.args.vq = _ytallhtml5.resolutionToYTQuality(resolution); 53 | } 54 | 55 | return window._ytplayer; 56 | }, 57 | set: function(value) { 58 | window._ytplayer = value; 59 | } 60 | }); 61 | }); 62 | 63 | // Apply the selected start options to the video, like start paused 64 | applyStartOptions(self.options.settings["yt-start-option"]); 65 | 66 | // Disable autoplay by updating the local cookies, which is necessary if 67 | // youtube ever writes this pref, because the local value overwrites the 68 | // value sent to the server (which is also modified) in this case 69 | if(self.options.settings["yt-disable-autoplay"]) { 70 | // extract PREF cookie (https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie) 71 | let pref = document.cookie.replace(/(?:(?:^|.*;\s*)PREF\s*\=\s*([^;]*).*$)|^.*$/, "$1"); 72 | if(pref) { 73 | let pattern = /f5=[^&]+/; 74 | if(-1 == pref.search(pattern)) { 75 | pref += "&f5=30030"; 76 | } else { 77 | pref = pref.replace(pattern, "f5=30030"); 78 | } 79 | // The domain is the same that is used by youtube to ensure that the 80 | // cookie is overwritten instead of just added alongside 81 | document.cookie = "PREF=" + pref + "; domain=.youtube.com"; 82 | } 83 | } 84 | 85 | // Manage theater mode (setting local cookie like above) 86 | if(-2 == self.options.settings["yt-player-height"]) { 87 | document.cookie = "wide=1; domain=.youtube.com"; 88 | } 89 | 90 | // This is called when the youtube player has finished loading 91 | // and its API can be used safely 92 | window.wrappedJSObject.onYouTubePlayerReady = function() { 93 | var player = document.querySelector("#movie_player"); 94 | if(!player) { 95 | return; 96 | } 97 | 98 | player = player.wrappedJSObject; 99 | 100 | // set the volume of the video if requested 101 | var volume = self.options.settings["yt-video-volume"]; 102 | if(volume !== -1) { 103 | player.setVolume(volume); 104 | } 105 | 106 | // set the playback rate of the video if requested 107 | var playbackRate = parseFloat(self.options.settings["yt-video-playback-rate"]); 108 | if(playbackRate !== 1) { 109 | player.setPlaybackRate(playbackRate); 110 | } 111 | } 112 | 113 | 114 | /** 115 | * Function that returns the 16:9 video resolution for a given player height. 116 | * Tries to calculate it based on the window size if playerHeight < 0. 117 | */ 118 | exportFunction(function(playerHeight) { // findBestResolution 119 | if(playerHeight < 0) { 120 | var sizesReverse = _ytallhtml5.options.playerHeights.slice().reverse(); 121 | for(var i in sizesReverse) { 122 | if(document.body.clientWidth >= (sizesReverse[i] * 16 / 9)) { 123 | playerHeight = sizesReverse[i]; 124 | break; 125 | } 126 | } 127 | if(playerHeight < 0) { 128 | return null; 129 | } 130 | } 131 | 132 | return "" + (playerHeight * 16 / 9) + "x" + playerHeight; 133 | }, _ytallhtml5, {defineAs: "findBestResolution"}); 134 | 135 | /** 136 | * Function that converts a given resolution to the youtube representation. 137 | */ 138 | exportFunction(function(resolution) { // resolutionToYTQuality 139 | switch(resolution) { 140 | case "256x144": return "light"; 141 | case "426x240": return "small"; 142 | case "640x360": return "medium"; 143 | case "853x480": return "large"; 144 | case "1280x720": return "hd720"; 145 | case "1920x1080": return "hd1080"; 146 | case "2560x1440": return "hd1440"; 147 | case "3840x2160": return "hd2160"; 148 | default: return "auto"; 149 | } 150 | }, _ytallhtml5, {defineAs: "resolutionToYTQuality"}); 151 | 152 | /** 153 | * Set up listeners to apply the video start options (eg paused). 154 | */ 155 | function applyStartOptions(startOption) { 156 | if("none" !== startOption) { 157 | if("paused-if-hidden" === startOption && !document.hidden) { 158 | // video is visible, so do not pause in this mode 159 | return; 160 | } 161 | var queryParameters = parseUrlQuery(document.location.search); 162 | if("paused-if-not-playlist" === startOption && ("list" in queryParameters)) { 163 | // we are watching a playlist, so do not pause in this mode 164 | return; 165 | } 166 | 167 | var listener = function(event) { 168 | document.documentElement.removeEventListener("playing", listener, true); 169 | event.target.pause(); 170 | }; 171 | document.documentElement.addEventListener("playing", listener, true); 172 | } 173 | } 174 | 175 | /** 176 | * Parse the query part of a url into a map 177 | * @see: http://www.techtricky.com/how-to-get-url-parameters-using-javascript/ 178 | */ 179 | function parseUrlQuery(query) { 180 | var params = {}; 181 | query.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(str, key, value) { 182 | params[key] = value; 183 | }); 184 | return params; 185 | } 186 | 187 | 188 | /** 189 | * This is needed because since firefox 33 it is no longer allowed for 190 | * content scripts to export complex objects to the page context (eg properties). 191 | * Because the function is serialized, references do not work. To pass static 192 | * data to the exported function, use the second parameter. (supports only 193 | * objects with basic types, not functions etc.) 194 | * Inspired by Rob W, http://stackoverflow.com/a/9517879 195 | */ 196 | function runInPageContext(func, data) { 197 | if(!(func instanceof Function) || func.name != "") { 198 | throw "Please use an anonymous function"; 199 | } 200 | 201 | var serializedData = ""; 202 | if(data instanceof Object) { 203 | serializedData += "JSON.parse(\""; 204 | serializedData += JSON.stringify(data).replace("\\", "\\\\", "g") 205 | .replace("\"", "\\\"", "g"); 206 | serializedData += "\")"; 207 | } 208 | 209 | var serializedFunc = "(" + func.toSource() + ")(" + serializedData + ")"; 210 | var script = document.createElement('script'); 211 | script.textContent = serializedFunc; 212 | 213 | var root = document.documentElement; 214 | root.appendChild(script); // script is run here ... 215 | root.removeChild(script); // ... so we can remove the tag directly afterwards 216 | } 217 | -------------------------------------------------------------------------------- /lib/CookieChanger.js: -------------------------------------------------------------------------------- 1 | const events = require("sdk/system/events"); 2 | const {Ci} = require("chrome"); 3 | 4 | // export CookieChanger 5 | exports.CookieChanger = CookieChanger; 6 | exports.Cookies = Cookies; 7 | exports.modifyPrefCookie = modifyPrefCookie; 8 | 9 | // save all CookieChangers 10 | var listeners = []; 11 | 12 | /** 13 | * Class to modify cookies on http requests 14 | * 15 | * @param host string The hostname for wich you want to change cookies 16 | * @param callback Function The function to modify the cookies 17 | */ 18 | function CookieChanger(host, callback) { 19 | this.host = host; 20 | this.callback = callback; 21 | this.active = false; 22 | 23 | listeners.push(this); 24 | } 25 | 26 | /** 27 | * Start changing cookies 28 | */ 29 | CookieChanger.prototype.register = function() { 30 | this.active = true; 31 | } 32 | 33 | /** 34 | * Stop changing cookies 35 | */ 36 | CookieChanger.prototype.unregister = function() { 37 | this.active = false; 38 | } 39 | 40 | 41 | /** 42 | * Class to parse the http request "Cookie" header 43 | */ 44 | function Cookies(cookieString) { 45 | this.cookies = new Map(); 46 | 47 | cookieString.split(/; */).forEach((cookie) => { 48 | var i = cookie.indexOf("="); 49 | 50 | if(i == -1) { 51 | return; 52 | } 53 | 54 | this.cookies.set(cookie.substring(0, i), cookie.substring(i + 1)); 55 | }); 56 | } 57 | 58 | /** 59 | * Serializes the Cookie object into a proper "Cookie" http header 60 | */ 61 | Cookies.prototype.unparse = function() { 62 | var cookieList = []; 63 | 64 | this.cookies.forEach((value, key) => { 65 | cookieList.push(key + "=" + value); 66 | }); 67 | 68 | return cookieList.join("; "); 69 | } 70 | 71 | /** 72 | * Check if the given cookie exists 73 | */ 74 | Cookies.prototype.has = function(key) { 75 | return this.cookies.has(key); 76 | } 77 | 78 | /** 79 | * Get the value of the given cookie 80 | */ 81 | Cookies.prototype.get = function(key) { 82 | return this.cookies.get(key); 83 | } 84 | 85 | /** 86 | * Set the cookie key to value 87 | */ 88 | Cookies.prototype.set = function(key, value) { 89 | this.cookies.set(key, value); 90 | } 91 | 92 | 93 | /** 94 | * Set single keys in the PREF cookie used by youtube to store settings 95 | * 96 | * @param cookies Cookies The cookies instance to modify 97 | * @param key string The key to set or replace if existing, eg. "f2" 98 | * @param value string The value for the given key 99 | */ 100 | function modifyPrefCookie(cookies, key, value) { 101 | var entry = key + "=" + value; 102 | 103 | if(cookies.has("PREF")) { 104 | var pref = cookies.get("PREF"); 105 | var pattern = new RegExp(key + "=[^&]+"); 106 | 107 | if(pref.length == 0) { 108 | pref = entry; 109 | } else if(-1 == pref.search(pattern)) { 110 | pref += "&" + entry; 111 | } else { 112 | pref = pref.replace(pattern, entry); 113 | } 114 | cookies.set("PREF", pref); 115 | } else { 116 | cookies.set("PREF", entry); 117 | } 118 | } 119 | 120 | 121 | // Event listener that calls the other registered listeners based on 122 | // the active state and the given hostname. 123 | // The cookie header is only parsed when at least one callback is applicable. 124 | function httpListener(event) { 125 | var request = event.subject.QueryInterface(Ci.nsIHttpChannel); 126 | var host = request.URI.host; 127 | 128 | var applicableListeners = listeners.filter((listener) => { 129 | return listener.active && listener.host === host; 130 | }); 131 | 132 | if(applicableListeners.length > 0) { 133 | var cookieString = ""; 134 | try { 135 | cookieString = request.getRequestHeader("Cookie"); 136 | } catch(ex) {} 137 | 138 | var cookies = new Cookies(cookieString); 139 | 140 | applicableListeners.forEach((listener) => { 141 | listener.callback(cookies); 142 | }); 143 | 144 | // setting an empty string (no cookies) deletes the header 145 | request.setRequestHeader("Cookie", cookies.unparse(), false); 146 | } 147 | } 148 | 149 | // Register httpListener function 150 | events.on("http-on-modify-request", httpListener); 151 | 152 | // Unregister httpListener on unload 153 | require("sdk/system/unload").when(function() { 154 | events.off("http-on-modify-request", httpListener); 155 | listeners = []; 156 | }); 157 | -------------------------------------------------------------------------------- /lib/PluginPermissionChanger.js: -------------------------------------------------------------------------------- 1 | const {Cu} = require("chrome"); 2 | 3 | Cu.import("resource://gre/modules/Services.jsm"); 4 | 5 | // plugin states 6 | const UNSET = 0; 7 | const ALLOW = 1; 8 | const DENY = 2; 9 | const CLICK_TO_PLAY = 3; 10 | 11 | // export PluginPermissionChanger 12 | exports.PluginPermissionChanger = PluginPermissionChanger; 13 | 14 | // save the observers to unload them on shutdown 15 | var observers = []; 16 | 17 | /** 18 | * Small wrapper class to change permissions of a plugin on a specific site. 19 | * 20 | * @param url String Complete URL of the website, eg "https://www.youtube.com" 21 | * @param plugin String Name of the plugin, eg "flash" 22 | */ 23 | function PluginPermissionChanger(uri, plugin) { 24 | this.uri = Services.io.newURI(uri, null, null); 25 | this.plugin = "plugin:" + plugin; 26 | } 27 | 28 | /** 29 | * Get the status of the plugin, corresponds to the other functions 30 | * 31 | * @return String One of "allow", "deny", "ask" and "unset" 32 | */ 33 | PluginPermissionChanger.prototype.status = function() { 34 | var perm = Services.perms.testPermission(this.uri, this.plugin); 35 | return permissionToString(perm); 36 | }; 37 | 38 | /** 39 | * Always execute the plugin 40 | */ 41 | PluginPermissionChanger.prototype.allow = function() { 42 | Services.perms.add(this.uri, this.plugin, ALLOW); 43 | }; 44 | 45 | /** 46 | * Never execute the plugin 47 | */ 48 | PluginPermissionChanger.prototype.deny = function() { 49 | Services.perms.add(this.uri, this.plugin, DENY); 50 | }; 51 | 52 | /** 53 | * Ask before executing the plugin 54 | */ 55 | PluginPermissionChanger.prototype.ask = function() { 56 | Services.perms.add(this.uri, this.plugin, CLICK_TO_PLAY); 57 | }; 58 | 59 | /** 60 | * Reset to default setting 61 | */ 62 | PluginPermissionChanger.prototype.reset = function() { 63 | try { 64 | // remove takes URI since firefox 42, see bug 1170200 65 | Services.perms.remove(this.uri, this.plugin); 66 | } catch(e) { 67 | // remove took an hostname before firefox 42 68 | Services.perms.remove(this.uri.asciiHost, this.plugin); 69 | } 70 | }; 71 | 72 | /** 73 | * Register a function that will be called when the permission is changed. 74 | * The callback gets passed the new state as its first argument. (see status()) 75 | * Multiple functions can be registered and do not have to be removed on unload. 76 | * 77 | * @param callback Function Callback function that takes one argument 78 | */ 79 | PluginPermissionChanger.prototype.onChange = function(callback) { 80 | var uri = this.uri; 81 | var type = this.plugin; 82 | 83 | var observer = { 84 | observe: function(subject, topic, data) { 85 | // subject no longer has a host propery in firefox 42 (see above) 86 | if(subject.type == type && 87 | ( 88 | (subject.host && subject.host == uri.asciiHost) || 89 | (subject.principal && subject.principal.URI.equals(uri)) 90 | )) { 91 | if("deleted" == data) { 92 | callback(permissionToString(UNSET)); 93 | } else { 94 | callback(permissionToString(subject.capability)); 95 | } 96 | } 97 | } 98 | }; 99 | 100 | // save for unload 101 | observers.push(observer); 102 | 103 | Services.obs.addObserver(observer, "perm-changed", false); 104 | }; 105 | 106 | 107 | /** 108 | * Helper function to turn the constants into strings 109 | */ 110 | function permissionToString(permission) { 111 | switch(permission) { 112 | case ALLOW: return "allow"; 113 | case DENY: return "deny"; 114 | case CLICK_TO_PLAY: return "ask"; 115 | } 116 | return "unset"; 117 | } 118 | 119 | /** 120 | * Remove observer on unload 121 | */ 122 | require("sdk/system/unload").when(function() { 123 | for(let observer of observers) { 124 | Services.obs.removeObserver(observer, "perm-changed"); 125 | } 126 | observers = []; 127 | }); 128 | -------------------------------------------------------------------------------- /lib/URIChanger.js: -------------------------------------------------------------------------------- 1 | const {Ci, Cu} = require('chrome'); 2 | 3 | Cu.import("resource://gre/modules/Services.jsm"); 4 | 5 | // export URIChanger 6 | exports.URIChanger = URIChanger; 7 | 8 | // save all object to unregister them on unload 9 | var objects = []; 10 | 11 | /** 12 | * Class to redirect a request based on a callback. (Note that the callback gets 13 | * called again for the rewritten uri) 14 | * 15 | * @param callback Function The function to modify the url; should return the new 16 | * (cloned) uri or null if no redirect is needed 17 | */ 18 | function URIChanger(callback) { 19 | this.callback = callback; 20 | 21 | // save for unregister on unload 22 | objects.push(this); 23 | } 24 | 25 | /** 26 | * Start changing the uri 27 | */ 28 | URIChanger.prototype.register = function() { 29 | if(!this.observer) { 30 | var observer = { 31 | callback: this.callback, 32 | observe: function(subject, topic, data) { 33 | var request = subject.QueryInterface(Ci.nsIHttpChannel); 34 | var newUri = this.callback(request.URI); 35 | if (newUri) { 36 | request.redirectTo(newUri); 37 | } 38 | } 39 | }; 40 | 41 | this.observer = observer; 42 | } 43 | 44 | Services.obs.addObserver(this.observer, "http-on-modify-request", false); 45 | } 46 | 47 | /** 48 | * Stop changing the uri. Does nothing when the changer is not registered 49 | */ 50 | URIChanger.prototype.unregister = function() { 51 | if(this.observer) { 52 | Services.obs.removeObserver(this.observer, "http-on-modify-request"); 53 | delete this.observer; 54 | } 55 | } 56 | 57 | /** 58 | * Unregister objects on unload 59 | */ 60 | require("sdk/system/unload").when(function() { 61 | for(let obj of objects) { 62 | obj.unregister(); 63 | } 64 | objects = []; 65 | }); 66 | -------------------------------------------------------------------------------- /lib/UserAgentChanger.js: -------------------------------------------------------------------------------- 1 | const {Ci, Cu} = require('chrome'); 2 | 3 | Cu.import("resource://gre/modules/Services.jsm"); 4 | 5 | // export UserAgentChanger 6 | exports.UserAgentChanger = UserAgentChanger; 7 | 8 | // save all object to unregister them on unload 9 | var objects = []; 10 | 11 | /** 12 | * Small wrapper to change the user agent that gets send to a specific host 13 | * 14 | * @param host String Host to change the user agent for, eg. "www.youtube.com" 15 | * @param userAgent String The new user agent to be send 16 | */ 17 | function UserAgentChanger(host, userAgent) { 18 | this.host = host; 19 | this.userAgent = userAgent; 20 | 21 | // save for unregister on unload 22 | objects.push(this); 23 | } 24 | 25 | /** 26 | * Start changing the user agent for the specified host 27 | */ 28 | UserAgentChanger.prototype.register = function() { 29 | if(!this.observer) { 30 | var observer = { 31 | host: this.host, 32 | userAgent: this.userAgent, 33 | observe: function(subject, topic, data) { 34 | var request = subject.QueryInterface(Ci.nsIHttpChannel); 35 | 36 | if(this.host == request.URI.host) { 37 | request.setRequestHeader("User-Agent", this.userAgent, false); 38 | } 39 | } 40 | }; 41 | 42 | this.observer = observer; 43 | } 44 | 45 | Services.obs.addObserver(this.observer, "http-on-modify-request", false); 46 | } 47 | 48 | /** 49 | * Stop changing the user agent. Does nothing when the changer is not registered 50 | */ 51 | UserAgentChanger.prototype.unregister = function() { 52 | if(this.observer) { 53 | Services.obs.removeObserver(this.observer, "http-on-modify-request"); 54 | delete this.observer; 55 | } 56 | } 57 | 58 | /** 59 | * Unregister objects on unload 60 | */ 61 | require("sdk/system/unload").when(function() { 62 | for(let obj of objects) { 63 | obj.unregister(); 64 | } 65 | objects = []; 66 | }); 67 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | const {data, id, version} = require("sdk/self"); 2 | const {viewFor} = require("sdk/view/core"); 3 | var pageMod = require("sdk/page-mod"); 4 | var simplePrefs = require("sdk/simple-prefs"); 5 | var contextMenu = require("sdk/context-menu"); 6 | var tabs = require("sdk/tabs"); 7 | 8 | const {UserAgentChanger} = require("./UserAgentChanger"); 9 | const {URIChanger} = require("./URIChanger"); 10 | const {PluginPermissionChanger} = require("./PluginPermissionChanger"); 11 | const {CookieChanger, modifyPrefCookie} = require("./CookieChanger"); 12 | 13 | // Internet Explorer 10 on Windows 7 14 | const IEUserAgent = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"; 15 | // available player sizes (see yt-player-height setting) 16 | const playerHeights = [360, 480, 720, 1080, 1440]; 17 | 18 | var youtubeIE = new UserAgentChanger("www.youtube.com", IEUserAgent); 19 | var youtubeFlash = new PluginPermissionChanger("https://www.youtube.com", "flash"); 20 | var youtubeEmbedFlash = new URIChanger(function(uri) { 21 | if(("www.youtube.com" !== uri.host && "www.youtube-nocookie.com" !== uri.host) || 22 | "/embed/" !== uri.path.substr(0,7) || 23 | -1 !== uri.path.indexOf("html5=1")) { 24 | return null; 25 | } 26 | var newUri = uri.clone(); 27 | newUri.path += (-1 === newUri.path.indexOf("?")) ? "?html5=1" : "&html5=1"; 28 | return newUri; 29 | }); 30 | var youtubeTheaterMode = new CookieChanger("www.youtube.com", (cookies) => { 31 | cookies.set("wide", "1"); 32 | }); 33 | var youtubeHTML5Test = new CookieChanger("www.youtube.com", (cookies) => { 34 | modifyPrefCookie(cookies, "f2", "40000000"); 35 | }); 36 | var youtubeDisableAutoplay = new CookieChanger("www.youtube.com", (cookies) => { 37 | // f5=20030 -> autoplay enabled (default) 38 | // f5=30030 -> autoplay disabled 39 | modifyPrefCookie(cookies, "f5", "30030"); 40 | }); 41 | 42 | function createContextMenuEntry() { 43 | return contextMenu.Item({ 44 | label: "Open video without playlist", 45 | image: data.url("html5-play.png"), 46 | context: contextMenu.SelectorContext("a[href]"), 47 | contentScriptFile: data.url("context-menu.js"), 48 | onMessage: function(video) { 49 | var currentTab = tabs.activeTab; 50 | var tabWindow = viewFor(currentTab.window); 51 | 52 | // open as a child of the current tab when TST is installed 53 | if("TreeStyleTabService" in tabWindow) { 54 | tabWindow.TreeStyleTabService.readyToOpenChildTab(viewFor(currentTab)); 55 | } 56 | 57 | tabs.open({ 58 | url: "https://www.youtube.com/watch?v=" + video 59 | }); 60 | } 61 | }); 62 | } 63 | 64 | var buttonContentScript = null; 65 | var videoContentScript = null; 66 | function createContentScripts(settings) { 67 | // destroy existing content scripts 68 | if(buttonContentScript && buttonContentScript.destroy) { 69 | buttonContentScript.destroy(); 70 | } 71 | if(videoContentScript && videoContentScript.destroy) { 72 | videoContentScript.destroy(); 73 | } 74 | 75 | // script for showing the button, resizing the player and disabling spf 76 | buttonContentScript = pageMod.PageMod({ 77 | include: /(http|https):\/\/www\.youtube\.com.*/, 78 | contentScriptWhen: "ready", 79 | contentScriptFile: data.url("youtube-html5.js"), 80 | contentStyleFile: data.url("youtube-html5.css"), 81 | contentScriptOptions: { 82 | buttonImageUrl: data.url("html5-play.png"), 83 | settingsImageUrl: data.url("settings.svg"), 84 | playerHeights: playerHeights, 85 | version: version, 86 | settings: settings 87 | }, 88 | onAttach: function(worker) { 89 | worker.port.on("openSettings", function() { 90 | require('sdk/window/utils').getMostRecentBrowserWindow().BrowserOpenAddonsMgr("addons://detail/" + id); 91 | }); 92 | } 93 | }); 94 | 95 | // script for setting the video resolution and volume and pausing the video 96 | videoContentScript = pageMod.PageMod({ 97 | include: /(http|https):\/\/www\.youtube\.com\/watch.*/, 98 | contentScriptWhen: "start", 99 | contentScriptFile: data.url("youtube-video-quality.js"), 100 | contentScriptOptions: { 101 | playerHeights: playerHeights, 102 | settings: settings 103 | } 104 | }); 105 | } 106 | 107 | 108 | exports.main = function(options) { 109 | var settings = {}; 110 | var openSingleVideo = null; 111 | 112 | // update setting on startup 113 | simplePrefs.prefs["yt-disable-flash"] = ("deny" == youtubeFlash.status()); 114 | 115 | // name change resolution -> yt-player-height 116 | if("resolution" in simplePrefs.prefs) { 117 | simplePrefs.prefs["yt-player-height"] = simplePrefs.prefs["resolution"]; 118 | delete simplePrefs.prefs["resolution"]; 119 | } 120 | 121 | // pref change: yt-start-paused(bool) -> yt-start-option(menulist) 122 | if("yt-start-paused" in simplePrefs.prefs) { 123 | simplePrefs.prefs["yt-start-option"] = 124 | simplePrefs.prefs["yt-start-paused"] ? "paused" : "none"; 125 | delete simplePrefs.prefs["yt-start-paused"]; 126 | } 127 | 128 | // pref change: yt-fix-volume(bool) -> yt-video-volume(menulist) 129 | if("yt-fix-volume" in simplePrefs.prefs) { 130 | if(simplePrefs.prefs["yt-fix-volume"]) { 131 | simplePrefs.prefs["yt-video-volume"] = 100; 132 | } 133 | delete simplePrefs.prefs["yt-fix-volume"]; 134 | } 135 | 136 | // change deleted api and embed and legacy ie method to default 137 | if("api" === simplePrefs.prefs["yt-loadtype"] || 138 | "iframe" === simplePrefs.prefs["yt-loadtype"] || 139 | "ie" === simplePrefs.prefs["yt-loadtype"]) { 140 | simplePrefs.prefs["yt-loadtype"] = "html5-test"; 141 | } 142 | 143 | settings["yt-player-height"] = simplePrefs.prefs["yt-player-height"]; 144 | settings["yt-video-resolution"] = simplePrefs.prefs["yt-video-resolution"]; 145 | settings["yt-loadtype"] = simplePrefs.prefs["yt-loadtype"]; 146 | settings["yt-disable-spf"] = simplePrefs.prefs["yt-disable-spf"]; 147 | settings["yt-disable-flash"] = simplePrefs.prefs["yt-disable-flash"]; 148 | settings["yt-start-option"] = simplePrefs.prefs["yt-start-option"]; 149 | settings["yt-video-playback-rate"] = simplePrefs.prefs["yt-video-playback-rate"]; 150 | settings["yt-video-volume"] = simplePrefs.prefs["yt-video-volume"]; 151 | settings["yt-disable-autoplay"] = simplePrefs.prefs["yt-disable-autoplay"]; 152 | settings["yt-hide-annotations"] = simplePrefs.prefs["yt-hide-annotations"]; 153 | settings["yt-enable-context-menu"] = simplePrefs.prefs["yt-enable-context-menu"]; 154 | 155 | createContentScripts(settings); 156 | 157 | if("ie-legacy" == settings["yt-loadtype"]) { 158 | youtubeIE.register(); 159 | } else if("html5-test" == settings["yt-loadtype"]) { 160 | youtubeHTML5Test.register(); 161 | } 162 | 163 | if (settings["yt-disable-flash"]) { 164 | youtubeEmbedFlash.register(); 165 | } 166 | 167 | if(-2 == settings["yt-player-height"]) { 168 | youtubeTheaterMode.register(); 169 | } 170 | 171 | if(settings["yt-enable-context-menu"]) { 172 | openSingleVideo = createContextMenuEntry(); 173 | } 174 | 175 | if(settings["yt-disable-autoplay"]) { 176 | youtubeDisableAutoplay.register(); 177 | } 178 | 179 | // listen for youtube flash permission changes 180 | youtubeFlash.onChange(function(newState) { 181 | settings["yt-disable-flash"] = simplePrefs.prefs["yt-disable-flash"] 182 | = ("deny" == newState); 183 | }); 184 | 185 | simplePrefs.on("", function(pref) { 186 | settings[pref] = simplePrefs.prefs[pref]; 187 | 188 | if("yt-loadtype" == pref) { 189 | if("ie-legacy" == settings["yt-loadtype"]) { 190 | youtubeHTML5Test.unregister(); 191 | youtubeIE.register(); 192 | } else if("html5-test" == settings["yt-loadtype"]) { 193 | youtubeIE.unregister(); 194 | youtubeHTML5Test.register(); 195 | } else { 196 | youtubeIE.unregister(); 197 | youtubeHTML5Test.unregister(); 198 | } 199 | } else if("yt-disable-flash" == pref) { 200 | if(settings["yt-disable-flash"]) { 201 | youtubeFlash.deny(); 202 | youtubeEmbedFlash.register(); 203 | } else if("deny" == youtubeFlash.status()) { 204 | youtubeFlash.reset(); 205 | youtubeEmbedFlash.unregister(); 206 | } 207 | } else if("yt-player-height" == pref) { 208 | if(-2 == settings["yt-player-height"]) { 209 | youtubeTheaterMode.register(); 210 | } else { 211 | youtubeTheaterMode.unregister(); 212 | } 213 | } else if("yt-enable-context-menu" == pref) { 214 | if(settings["yt-enable-context-menu"]) { 215 | openSingleVideo = createContextMenuEntry(); 216 | } else { 217 | openSingleVideo.destroy(); 218 | openSingleVideo = null; 219 | } 220 | } else if("yt-disable-autoplay" == pref) { 221 | if(settings["yt-disable-autoplay"]) { 222 | youtubeDisableAutoplay.register(); 223 | } else { 224 | youtubeDisableAutoplay.unregister(); 225 | } 226 | } 227 | 228 | // recreate content scripts, so they work with the new settings 229 | // (will destroy existing scripts, but not attach to existing tabs) 230 | createContentScripts(settings); 231 | }); 232 | } 233 | 234 | exports.onUnload = function(reason) { 235 | if("disable" == reason) { 236 | youtubeFlash.reset(); 237 | } 238 | }; 239 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klemens/ff-youtube-all-html5/2f8faa4584b8691af64a5ac2f79fe2057cc03fb8/logo.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | YouTube ALL HTML5 Logo 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-all-html5", 3 | "title": "YouTube ALL HTML5", 4 | "id": "jid1-qj0w91o64N7Eeg@jetpack", 5 | "description": "Play all videos on YouTube with your preferred settings (size, quality, playback rate, …) without cookies using only HTML5.", 6 | "author": "Klemens Schölhorn", 7 | "main" : "lib/main.js", 8 | "permissions": { "private-browsing": true, 9 | "unsafe-content-script": true, 10 | "multiprocess": true }, 11 | "license": "GPLv3", 12 | "version": "3.2.0", 13 | "engines": { 14 | "firefox": ">=38.0a1", 15 | "{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}": ">=27.1.0b1" 16 | }, 17 | "preferences": [{ 18 | "name": "yt-player-height", 19 | "title": "YouTube player size", 20 | "description": "Automatically change the size of the YouTube player.", 21 | "type": "menulist", 22 | "value": 0, 23 | "options" : [{ 24 | "label": "Do not change", 25 | "value": "0" 26 | }, { 27 | "label": "Calculate from window size", 28 | "value": "-1" 29 | }, { 30 | "label": "640x360 (360p)", 31 | "value": "360" 32 | }, { 33 | "label": "853x480 (480p)", 34 | "value": "480" 35 | }, { 36 | "label": "1280x720 (720p)", 37 | "value": "720" 38 | }, { 39 | "label": "1920x1080 (1080p)", 40 | "value": "1080" 41 | }, { 42 | "label": "2560x1440 (1440p)", 43 | "value": "1440" 44 | }, { 45 | "label": "Use theater mode", 46 | "value": "-2" 47 | }] 48 | }, { 49 | "name": "yt-video-resolution", 50 | "title": "YouTube video resolution", 51 | "description": "Choose the video quality to load.\n\"Auto\" chooses the right resolution for the size selected above.", 52 | "type": "menulist", 53 | "value": "auto", 54 | "options" : [{ 55 | "label": "Auto", 56 | "value": "auto" 57 | }, { 58 | "label": "144p", 59 | "value": "256x144" 60 | }, { 61 | "label": "240p", 62 | "value": "426x240" 63 | }, { 64 | "label": "360p", 65 | "value": "640x360" 66 | }, { 67 | "label": "480p (MSE)", 68 | "value": "853x480" 69 | }, { 70 | "label": "720p", 71 | "value": "1280x720" 72 | }, { 73 | "label": "1080p (MSE)", 74 | "value": "1920x1080" 75 | }, { 76 | "label": "1440p (MSE)", 77 | "value": "2560x1440" 78 | }, { 79 | "label": "2160p (MSE)", 80 | "value": "3840x2160" 81 | }] 82 | }, { 83 | "name": "yt-disable-flash", 84 | "title": "Disable Flash on YouTube", 85 | "description": "If you want to keep Flash enabled but use HTML5 on YouTube (and embedded videos).", 86 | "type": "bool", 87 | "value": false 88 | }, { 89 | "name": "yt-start-option", 90 | "title": "Start videos paused", 91 | "description": "This pauses videos when they are loaded", 92 | "type": "menulist", 93 | "value": "none", 94 | "options" : [{ 95 | "label": "Never", 96 | "value": "none" 97 | }, { 98 | "label": "Only in background tabs", 99 | "value": "paused-if-hidden" 100 | }, { 101 | "label": "Only when not watching a playlist", 102 | "value": "paused-if-not-playlist" 103 | }, { 104 | "label": "Always", 105 | "value": "paused" 106 | }] 107 | }, { 108 | "name": "yt-video-playback-rate", 109 | "title": "Video playback rate", 110 | "description": "Set playback rate on start of the video (experimental)", 111 | "type": "menulist", 112 | "value": "1.0", 113 | "options" : [{ 114 | "label": "Slower (0.25x)", 115 | "value": "0.25" 116 | }, { 117 | "label": "Slow (0.5x)", 118 | "value": "0.5" 119 | }, { 120 | "label": "Default (do not change)", 121 | "value": "1.0" 122 | }, { 123 | "label": "Fast (1.25x)", 124 | "value": "1.25" 125 | }, { 126 | "label": "Faster (1.5x)", 127 | "value": "1.5" 128 | }, { 129 | "label": "Ludicrous (2x)", 130 | "value": "2.0" 131 | }] 132 | }, { 133 | "name": "yt-video-volume", 134 | "title": "Video volume", 135 | "description": "Set volume on start of the video (experimental)", 136 | "type": "menulist", 137 | "value": -1, 138 | "options" : [{ 139 | "label": "Default (do not change)", 140 | "value": "-1" 141 | }, { 142 | "label": "0 % (Muted)", 143 | "value": "0" 144 | }, { 145 | "label": "5 %", 146 | "value": "5" 147 | }, { 148 | "label": "12 %", 149 | "value": "12" 150 | }, { 151 | "label": "25 %", 152 | "value": "25" 153 | }, { 154 | "label": "50 %", 155 | "value": "50" 156 | }, { 157 | "label": "75 %", 158 | "value": "75" 159 | }, { 160 | "label": "100 %", 161 | "value": "100" 162 | }] 163 | }, { 164 | "name": "yt-disable-autoplay", 165 | "title": "Prevent autoplay of recommended videos", 166 | "description": "This is equivalent to the button in the recommended column (experimental)", 167 | "type": "bool", 168 | "value": false 169 | }, { 170 | "name": "yt-hide-annotations", 171 | "title": "Hide video annotations", 172 | "description": "This hides all text and link annotations (experimental)", 173 | "type": "bool", 174 | "value": false 175 | }, { 176 | "name": "yt-enable-context-menu", 177 | "title": "Enable opening videos without their playlists", 178 | "description": "Adds a context menu entry that lets you open a video link which contains a playlist without the playlist.", 179 | "type": "bool", 180 | "value": true 181 | }, { 182 | "name": "yt-loadtype", 183 | "title": "YouTube video loading method", 184 | "type": "radio", 185 | "value": "html5-test", 186 | "options": [{ 187 | "value": "html5-test", 188 | "label": "Force HTML5-Test (default)" 189 | }, { 190 | "value": "none", 191 | "label": "Disable" 192 | }, { 193 | "value": "ie-legacy", 194 | "label": "Emulate Internet Explorer (not recommended)" 195 | }] 196 | }, { 197 | "name": "yt-disable-spf", 198 | "title": "Disable YouTube SPF", 199 | "description": "Most features of this add-on only work correctly when SPF is disabled", 200 | "type": "bool", 201 | "value": true 202 | }] 203 | } 204 | -------------------------------------------------------------------------------- /test/test-cookies.js: -------------------------------------------------------------------------------- 1 | const {Cookies, modifyPrefCookie} = require("../lib/CookieChanger"); 2 | 3 | exports.testCookieParsing = (assert) => { 4 | var t = (cookieString) => { 5 | return (new Cookies(cookieString)).unparse(); 6 | }; 7 | 8 | assert.equal(t(""), ""); 9 | assert.equal(t("a=b"), "a=b"); 10 | assert.equal(t("a=1; b=2"), "a=1; b=2"); 11 | assert.equal(t("b=2; a=1"), "b=2; a=1"); 12 | 13 | assert.equal(t("a=1; b=2"), "a=1; b=2"); 14 | assert.equal(t("a=1; ; b=2"), "a=1; b=2"); 15 | assert.equal(t("a=1;; b=2"), "a=1; b=2"); 16 | assert.equal(t("a=1;;b=2"), "a=1; b=2"); 17 | assert.equal(t("a=1 ;b=2"), "a=1 ; b=2"); 18 | 19 | // Cookies uses a Map as its backend 20 | assert.equal(t("a=1; a=2"), "a=2"); 21 | 22 | assert.equal(t("abc"), ""); 23 | assert.equal(t("abc="), "abc="); 24 | assert.equal(t("=abc"), "=abc"); 25 | assert.equal(t("a=1; abc; b=2"), "a=1; b=2"); 26 | }; 27 | 28 | exports.testModifyPref = (assert) => { 29 | var t = (testcase, key, value) => { 30 | var cookies = new Cookies(testcase); 31 | modifyPrefCookie(cookies, key, value); 32 | return cookies.unparse(); 33 | }; 34 | 35 | var t1 = "PREF=f1=abc&f2=999"; 36 | assert.equal(t(t1, "f1", "1234"), "PREF=f1=1234&f2=999"); 37 | assert.equal(t(t1, "f2", "abcd"), "PREF=f1=abc&f2=abcd"); 38 | assert.equal(t(t1, "f3", "123"), "PREF=f1=abc&f2=999&f3=123"); 39 | assert.equal(t(t1, "f1", ""), "PREF=f1=&f2=999"); 40 | assert.equal(t(t1, "f2", ""), "PREF=f1=abc&f2="); 41 | 42 | assert.equal(t("PREF=", "f2", "1"), "PREF=f2=1"); 43 | assert.equal(t("", "f2", "1"), "PREF=f2=1"); 44 | }; 45 | 46 | require('sdk/test').run(exports); 47 | --------------------------------------------------------------------------------