├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------