├── .gitignore ├── icons └── icon-128.png ├── images ├── chat-screenshot.png ├── chrome-window.png ├── developer-mode.png ├── get-overlay-url.png └── load-unpacked.png ├── manifest.json ├── settings ├── options.js └── options.html ├── README.md ├── youtube.css ├── youtube.js └── jquery.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/live-chat-overlay/main/icons/icon-128.png -------------------------------------------------------------------------------- /images/chat-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/live-chat-overlay/main/images/chat-screenshot.png -------------------------------------------------------------------------------- /images/chrome-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/live-chat-overlay/main/images/chrome-window.png -------------------------------------------------------------------------------- /images/developer-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/live-chat-overlay/main/images/developer-mode.png -------------------------------------------------------------------------------- /images/get-overlay-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/live-chat-overlay/main/images/get-overlay-url.png -------------------------------------------------------------------------------- /images/load-unpacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronpk/live-chat-overlay/main/images/load-unpacked.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Live Chat Overlay", 3 | "description": "Restyle the YouTube popout chat so you can overlay it for livestreams", 4 | "manifest_version": 3, 5 | "version": "0.3.9", 6 | "homepage_url": "https://github.com/aaronpk/live-chat-overlay", 7 | "icons": { 8 | "128": "icons/icon-128.png" 9 | }, 10 | "permissions": [ 11 | "storage" 12 | ], 13 | "host_permissions": [ 14 | "https://youtube.com/*", "https://www.youtube.com/*", "https://studio.youtube.com/*" 15 | ], 16 | "content_scripts": [{ 17 | "css": ["youtube.css"], 18 | "js": ["jquery.js", "youtube.js"], 19 | "all_frames": false, 20 | "matches": ["https://youtube.com/live_chat*", "https://www.youtube.com/live_chat*", "https://studio.youtube.com/live_chat*"] 21 | }], 22 | "options_ui": { 23 | "page": "settings/options.html" 24 | }, 25 | "browser_specific_settings": { 26 | "gecko": { 27 | "id": "live-chat-overlay@aaronpk.tv", 28 | "strict_min_version": "80.0" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /settings/options.js: -------------------------------------------------------------------------------- 1 | function generateSessionID(){ 2 | var text = ""; 3 | var chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; 4 | for (var i = 0; i < 10; i++){ 5 | text += chars.charAt(Math.floor(Math.random() * chars.length)); 6 | } 7 | return text; 8 | }; 9 | 10 | function toggleSessionID() { 11 | if (this.checked) { 12 | document.querySelector("#session-id").value = generateSessionID(); 13 | } else { 14 | document.querySelector("#session-id").value = ""; 15 | } 16 | } 17 | 18 | 19 | function saveOptions(e) { 20 | e.preventDefault(); 21 | chrome.storage.sync.set({ 22 | color: document.querySelector("#color").value, 23 | scale: document.querySelector("#scale").value, 24 | sizeOffset: document.querySelector("#size-offset").value, 25 | commentBottom: document.querySelector("#comment-bottom").value, 26 | commentHeight: document.querySelector("#comment-height").value, 27 | commentBackgroundColor: document.querySelector("#comment-bg-color").value, 28 | commentColor: document.querySelector("#comment-color").value, 29 | authorBackgroundColor: document.querySelector("#author-bg-color").value, 30 | authorAvatarBorderColor: document.querySelector("#author-avatar-border-color").value, 31 | authorAvatarOverlayOpacity: document.querySelector("#author-avatar-overlay-opacity").value, 32 | authorColor: document.querySelector("#author-color").value, 33 | fontFamily: document.querySelector("#font-family").value, 34 | highlightWords: document.querySelector("#highlight-words").value.toLowerCase().replace(/[^a-z0-9, ]/gi, '').split(",").map(e => e.trim()), 35 | showOnlyFirstName: document.querySelector("#firstname").checked, 36 | autoHideSeconds: document.querySelector("#auto-hide-seconds").value, 37 | popoutURL: document.querySelector("#popout-url").value, 38 | serverURL: document.querySelector("#server-url").value, 39 | persistentSessionID: document.querySelector("#persistent-session-id").checked, 40 | sessionID: document.querySelector("#session-id").value 41 | }); 42 | } 43 | 44 | function restoreOptions() { 45 | 46 | var properties = ["color","scale","commentBottom","commentHeight","sizeOffset","authorBackgroundColor","authorAvatarBorderColor","authorColor","commentBackgroundColor","commentColor","fontFamily","showOnlyFirstName","highlightWords","popoutURL","serverURL","autoHideSeconds","authorAvatarOverlayOpacity","persistentSessionID","sessionID"]; 47 | chrome.storage.sync.get(properties, function(result){ 48 | document.querySelector("#color").value = result.color || "#000"; 49 | document.querySelector("#scale").value = result.scale || "1.0"; 50 | document.querySelector("#size-offset").value = result.sizeOffset || "0"; 51 | document.querySelector("#comment-bottom").value = result.commentBottom || "10px"; 52 | document.querySelector("#comment-height").value = result.commentHeight || "30vh"; 53 | document.querySelector("#author-bg-color").value = result.authorBackgroundColor || "#ffa500"; 54 | document.querySelector("#author-avatar-border-color").value = result.authorAvatarBorderColor || "#ffa500"; 55 | document.querySelector("#author-avatar-overlay-opacity").value = result.authorAvatarOverlayOpacity || "0.1"; 56 | document.querySelector("#author-color").value = result.authorColor || "#222"; 57 | document.querySelector("#comment-bg-color").value = result.commentBackgroundColor || "#222"; 58 | document.querySelector("#comment-color").value = result.commentColor || "#fff"; 59 | document.querySelector("#font-family").value = result.fontFamily || "Avenir Next, Helvetica, Geneva, Verdana, Arial, sans-serif"; 60 | document.querySelector("#firstname").checked = result.showOnlyFirstName || false; 61 | document.querySelector("#auto-hide-seconds").value = result.autoHideSeconds || 0; 62 | document.querySelector("#highlight-words").value = result.highlightWords.join(", ") || "q, question"; 63 | document.querySelector("#popout-url").value = result.popoutURL || "https://chat.aaronpk.tv/overlay/"; 64 | document.querySelector("#server-url").value = result.serverURL || "https://chat.aaronpk.tv/overlay/pub"; 65 | document.querySelector("#persistent-session-id").checked = result.persistentSessionID || false; 66 | document.querySelector("#session-id").value = result.sessionID || ""; 67 | }); 68 | 69 | document.querySelector("#persistent-session-id").addEventListener("change", toggleSessionID); 70 | } 71 | 72 | document.addEventListener("DOMContentLoaded", restoreOptions); 73 | document.querySelector("form").addEventListener("submit", saveOptions); 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Live Chat Overlay 2 | ================= 3 | 4 | This browser extension turns the YouTube popout chat window into something that can be used to show chat comments keyed over a video. You can also bring it in to software like OBS or Lightstream Studio as a browser source! 5 | 6 | ![chat-screenshot](images/chat-screenshot.png) 7 | 8 | [Setup Tutorial](https://youtu.be/HwctGtdsHZI) 9 | 10 | ## Installation 11 | 12 | ### Google Chrome Store 13 | 14 | The easiest way to install the extension from the Google Chrome store. Click the link below and you should see a prompt to install it in Chrome. 15 | 16 | * https://chrome.google.com/webstore/detail/live-chat-overlay/aplaefbnohemkmngdogmbkpompjlijia 17 | 18 | 19 | ### Manual Installation 20 | 21 | First, [download this project](https://github.com/aaronpk/live-chat-overlay/archive/refs/heads/main.zip) and unzip it somewhere on your computer. In Chrome, launch the Extensions page. 22 | 23 | Go to **Window** and click **Extensions** (or enter this in your address bar `chrome://extensions/`) 24 | 25 | ![window-extensions](images/chrome-window.png) 26 | 27 | Turn on **Developer Mode** 28 | 29 | ![developer-mode](images/developer-mode.png) 30 | 31 | That will show a new set of options. Click "Load Unpacked" 32 | 33 | ![load-unpacked](images/load-unpacked.png) 34 | 35 | Choose the folder you downloaded the extension into, and you should see it now in your list of extensions! 36 | 37 | ## Usage 38 | 39 | Open up the YouTube live chat for a video, and click YouTube's "popout chat" button to open it in a new window. Or replace the `VIDEOID` in the URL below with your video's ID. 40 | 41 | `https://www.youtube.com/live_chat?is_popout=1&v=VIDEOID` 42 | 43 | ### Keying from a Computer 44 | 45 | You'll next need to bring that into your video stream and key it out, which will depend on what software or hardware you are using. In the ATEM Mini, you can use these settings in the upstream keyer: 46 | 47 | * Luma key 48 | * not premultiplied 49 | * clip: 7% 50 | * gain: 100% 51 | 52 | Or you can use the downstream keyer: 53 | 54 | * not premultiplied 55 | * mask: 56 | * top: X 57 | * bottom: -9 58 | * left: -16 59 | * right: 16 60 | 61 | ### Adding as a Browser Source 62 | 63 | In software such as [OBS](https://obsproject.com) or [Lightstream Studio](http://strea.mr/aaronparecki) you can pop out a remote window as a browser source, and remote control it from your main YouTube chat window. 64 | 65 | Before you start, you'll want to make sure you change the background color in the extension settings to the word "transparent" so that the browser window will have a transparent background. 66 | 67 | Once you've loaded the YouTube popout chat window described above, you will see a button in the YouTube chat window called "Get Overlay URL". Clicking that will reveal a URL you can copy and load into OBS or your favorite streaming platform that supports browser overlay sources. 68 | 69 | ![get-overlay-url](images/get-overlay-url.png) 70 | 71 | You can open the URL on your computer too if you want to full screen it on a second monitor. 72 | 73 | ### Persistent Links 74 | 75 | There is now an option for "Persistent Session ID" to enable a persistent link for your stream so that you don't have to re-add the browser source every time. Here is how to use it. 76 | 77 | In the options form, check "Persistent Session ID" which will show a text box with a randomly generated Session ID. This Session ID will be used for all you future chats. You can also change the Session ID in that field if you want to reuse one that you already saved in a browser source. 78 | 79 | Note: You should always use a randomly generated ID. If you use a short word like "test", then the chats will be combined with anyone else who has used the same short word. 80 | 81 | 82 | ## See this in action! 83 | 84 | You can see this in action on many of [Aaron Parecki's livestreams](https://www.youtube.com/watch?v=CHQITWm5wDQ&list=PLRyLn6THA5wPracMVE74IHovBT3ebcsJV)! 85 | 86 | 87 | ## Running your own remote server 88 | 89 | The extension defaults to pushing the chat messages through a server managed by [Aaron Parecki](https://aaronpk.tv), and the remote window is loaded from that website. The chat messages aren't stored on the server, and there is very little resource usage for this, but if you are more comfortable running this on your own server, head over to the [overlay remote GitHub project](https://github.com/aaronpk/live-chat-overlay-remote) for instructions. 90 | 91 | 92 | ## TODO 93 | 94 | See https://github.com/aaronpk/youtube-chat-overlay/issues 95 | 96 | 97 | ## Credits 98 | 99 | The original CSS and JavaScript originally came from a video by [ROJ BTS](https://www.youtube.com/watch?v=NHy9D4ClTvc), so huge thanks to him for the initial work! 100 | 101 | -------------------------------------------------------------------------------- /settings/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 43 | 44 | 45 | 46 |

Customize the colors of the live chat overlay. Enter the hex codes of the colors you'd like to use. Remember that full black #000 will be keyed out. You can delete a value to reset it to the default.

47 | 52 | 53 |
54 |
55 |
56 | 57 |
Enter a list of comma-separated words that will be highlighted in the chat
58 | 59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 |
87 | 88 | 89 |
90 |
91 | 92 | 93 |
94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 |
111 | 112 | 113 |
114 |
115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 | 123 | 124 |
125 |
126 |
127 |
128 |

Adjust these to make the comment text smaller or fit more lines of text. Sorry it's kind of hard to use.

129 |
130 | 131 | 132 |
133 |
134 | 135 |
(set negative to move lower when scaled down, e.g. -2vh)
136 | 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 |
(make text area wider when scaled down, e.g. 100)
145 | 146 |
147 |
148 | 149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /youtube.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* basic colors */ 3 | --keyer-bg-color: #000; /* change for a chroma key if you want, beware profile avatars may key out */ 4 | --comment-color: #fff; 5 | --comment-bg-color: #222; /* needs to be slightly above black to not get keyed out */ 6 | --comment-border-radius: 0px; 7 | --comment-font-size: 40px; 8 | --author-color: #222; 9 | --author-bg-color: #ffa500; 10 | --author-avatar-border-color: #ffa500; 11 | --author-border-radius: 0px; 12 | --author-avatar-border-size: 3px; 13 | --author-avatar-size: 128px; 14 | --author-font-size: 30px; 15 | --author-avatar-overlay-opacity: 0.1; 16 | /* donation/superchat specific */ 17 | --donation-color: #5a4211; 18 | --donation-bg-color: #fff; 19 | --donation-gradient-stop0: #BF953F; 20 | --donation-gradient-stop1: #ede599; 21 | --donation-gradient-stop2: #B38728; 22 | --donation-label-color: #fff; 23 | --donation-label-text: 'SUPERCHAT'; 24 | --donation-shadow-color: #fff; 25 | 26 | --active-comment-bg-color: #90dd91; 27 | --shown-comment-bg-color: #555555; 28 | --highlighted-comment-bg-color: #f0f07d; 29 | 30 | /* 31 | LAYOUT 32 | */ 33 | --comment-width: auto; /* go 100% for a full screen lower third style*/ 34 | --comment-padding: 20px 40px 20px 70px; 35 | --comment-area-height: 30vh; 36 | --comment-scale: 1; 37 | --comment-area-size-offset: 0; 38 | --comment-area-bottom: 10px; 39 | /* 40 | STYLE 41 | */ 42 | --font-family: Avenir Next, Helvetica, Geneva, Verdana, Arial, sans-serif; 43 | --highlight-chat-font-weight: 600; 44 | --author-font-weight: 700; 45 | --comment-font-weight: 600; 46 | } 47 | 48 | body { 49 | background-color: var(--keyer-bg-color); 50 | } 51 | body.inline-chat yt-live-chat-app { 52 | margin-bottom: var(--comment-area-height); 53 | height: calc(100vh - var(--comment-area-height)) !important; 54 | } 55 | .btn-clear { 56 | position: absolute; 57 | z-index: 99999; 58 | right: 20px; 59 | font-size: 30px; 60 | border: 1px #bbb solid; 61 | border-radius: 4px; 62 | color: #fff; 63 | background: #444; 64 | bottom: 60px; 65 | } 66 | body.inline-chat .btn-clear { 67 | bottom: calc(var(--comment-area-height) + 60px); 68 | } 69 | 70 | .highlighted-comment { 71 | background-color: var(--highlighted-comment-bg-color) !important; 72 | } 73 | html[dark] .highlighted-comment #message.yt-live-chat-text-message-renderer { 74 | color: black; 75 | } 76 | html[dark] .highlighted-comment:hover #message.yt-live-chat-text-message-renderer { 77 | color: white; 78 | } 79 | html[dark] .highlighted-comment #author-name.yt-live-chat-author-chip { 80 | color: #444; 81 | } 82 | html[dark] .highlighted-comment:hover #author-name.yt-live-chat-author-chip { 83 | color: #DDD; 84 | } 85 | yt-live-chat-text-message-renderer:hover { 86 | background-color: #eee !important; 87 | } 88 | html[dark] yt-live-chat-text-message-renderer:hover { 89 | background-color: #444 !important; 90 | } 91 | yt-live-chat-text-message-renderer.shown-comment { 92 | background-color: var(--shown-comment-bg-color) !important; 93 | opacity: 0.4; 94 | } 95 | .shown-comment:hover { 96 | opacity: 0.5; 97 | } 98 | yt-live-chat-text-message-renderer.active-comment { 99 | background-color: var(--active-comment-bg-color) !important; 100 | opacity: 1; 101 | } 102 | highlight-chat { 103 | font-family: var(--font-family); 104 | font-weight: var(--highlight-chat-font-weight); 105 | box-sizing: border-box; 106 | display: block; 107 | position: absolute; 108 | bottom: var(--comment-area-bottom); 109 | left: calc(var(--comment-area-size-offset) * -1px); 110 | right: calc(var(--comment-area-size-offset) * -1px); 111 | width: auto; 112 | height: var(--comment-area-height); 113 | z-index:99999999999; 114 | overflow: hidden; 115 | margin: 0px; 116 | padding: 40px 50px 40px 220px; 117 | background-color: var(--keyer-bg-color); 118 | color: #fff; 119 | font-size: 30px; 120 | transform: scale(var(--comment-scale)); 121 | } 122 | highlight-chat.preview { 123 | border: 1px #ccc solid; 124 | } 125 | .hl-c-cont { 126 | position: relative; 127 | padding: 20px; 128 | width: 100%; 129 | margin: 0 auto; 130 | transition: .5s all cubic-bezier(0.250, 0.250, 0.105, 1.2); 131 | } 132 | .hl-c-cont.fadeout { 133 | transform: translateY(600px); 134 | } 135 | 136 | .hl-name { 137 | position: absolute; 138 | top: -20px; 139 | left: 50px; 140 | font-weight: var(--author-font-weight); 141 | background: var(--author-bg-color); 142 | color: var(--author-color); 143 | padding: 10px; 144 | transform: rotate(-0deg); 145 | z-index: 1; 146 | border-radius: var(--author-border-radius); 147 | font-size: var(--author-font-size); 148 | } 149 | .hl-name::before { 150 | content: ''; 151 | background: #f6cc0c; 152 | } 153 | .hl-message { 154 | position: absolute; 155 | width: var(--comment-width); 156 | font-weight: var(--comment-font-weight); 157 | padding: var(--comment-padding); 158 | color: var(--comment-color); 159 | background-color: var(--comment-bg-color); 160 | border-radius: var(--comment-border-radius); 161 | font-size: var(--comment-font-size); 162 | } 163 | .hl-message img { 164 | width: 50px; 165 | vertical-align: middle; 166 | } 167 | .hl-message img.sticker { 168 | width: 120px; 169 | } 170 | .hl-message a { 171 | color: white; 172 | } 173 | .hl-img { 174 | position: absolute; 175 | top: 0; 176 | z-index: 1; 177 | left: -60px; 178 | width: var(--author-avatar-size); 179 | height: var(--author-avatar-size); 180 | background-color: var(--author-avatar-border-color); 181 | border-radius: 50%; 182 | border: 0; 183 | padding: var(--author-avatar-border-size); 184 | } 185 | .hl-img img { 186 | display: block; 187 | width: 100%; 188 | border-radius: 50%; 189 | z-index: 1; 190 | } 191 | /* overlay a very faint white layer to bump the blacks above the luma key cutoff */ 192 | .hl-img:after { 193 | content: ''; 194 | display: block; 195 | height: 100%; 196 | width: 100%; 197 | top: 0; 198 | left: 0; 199 | border-radius: 50%; 200 | position:absolute; 201 | pointer-events: none; 202 | z-index: 3; 203 | } 204 | .hl-img:after { 205 | background: rgba(255,255,255,var(--author-avatar-overlay-opacity)); 206 | mix-blend-mode: lighten; 207 | } 208 | 209 | .hl-badges { 210 | display: inline-block; 211 | margin-left: 10px; 212 | } 213 | .hl-badges img.yt-live-chat-author-badge-renderer { 214 | width: 24px; 215 | height: 24px; 216 | } 217 | 218 | .donation { 219 | position: absolute; 220 | display: block; 221 | text-align: center; 222 | left: 10px; 223 | top: 108px; 224 | z-index: 3; 225 | min-width: 128px; 226 | border-radius: 10px; 227 | padding: 30px 5px 0; 228 | overflow: hidden; 229 | background: linear-gradient(to right, var(--donation-gradient-stop0), var(--donation-gradient-stop1), var(--donation-gradient-stop2)); 230 | color: var(--donation-color); 231 | transform: rotate(-5deg) translateX(-50%); 232 | } 233 | 234 | .donation.silver { 235 | background: linear-gradient(to right, #d6d6d6, #f1f1f1, #d6d6d6); 236 | color: #434343; 237 | } 238 | .donation.gold { 239 | background: linear-gradient(to right, #BF953F, #FCF6BA, #d0aa57, #FBF5B7, #AA771C); 240 | color: #4d3400; 241 | } 242 | .donation.platinum { 243 | background: 244 | linear-gradient( 245 | -72deg, 246 | #dedeff, 247 | #ffffff 16%, 248 | #dedeff 21%, 249 | #ffffff 24%, 250 | #555564 27%, 251 | #dedeff 36%, 252 | #ffffff 45%, 253 | #ffffff 60%, 254 | #dedeff 72%, 255 | #ffffff 80%, 256 | #dedeff 84%, 257 | #555564); 258 | color: #333; 259 | } 260 | .donation.diamond { 261 | background: linear-gradient(to right, #e5f1fa, #ffffff, #dbe7ec); 262 | color: #00374d; 263 | } 264 | .donation.emerald { 265 | background: linear-gradient(to right, #3fbf3f, #99ed9a, #3fbf3f); 266 | color: #115a14; 267 | } 268 | 269 | 270 | .membership { 271 | padding: 10px 10px 5px 10px; 272 | font-size: 0.8em; 273 | } 274 | .membership-length { 275 | font-size: 0.8em; 276 | } 277 | .donation::before { 278 | position: absolute; 279 | top: 0; 280 | left: 0; 281 | width: 100%; 282 | font-size: 18px; 283 | text-align: center; 284 | background-color: rgba(0, 0, 0, 0.23); 285 | border-radius: 10px 10px 0 0; 286 | padding: 5px 0 0; 287 | display: block; 288 | content: var(--donation-label-text); 289 | color: var(--donation-label-color); 290 | } 291 | .membership::before { 292 | content: ''; 293 | } 294 | .donation::after { 295 | content: ''; 296 | position: absolute; 297 | top: -50%; 298 | left: 0; 299 | height: 200%; 300 | width: 1px; 301 | background-color: var(--donation-bg-color); 302 | box-shadow: 0 0 20px 20px var(--donation-shadow-color); 303 | opacity: 0.7; 304 | transform: rotate(9deg) translate3D(250px, 0, 0); 305 | animation: superchat 3s ease-in-out infinite; 306 | } 307 | @keyframes superchat { 308 | from { 309 | transform: rotate(9deg) translate3D(-250px, 0, 0); 310 | } 311 | } 312 | 313 | /* hide chat input to give more space to the chat */ 314 | /* 315 | #input-panel, yt-live-chat-viewer-engagement-message-renderer { 316 | display: none !important; 317 | } 318 | */ 319 | 320 | yt-live-chat-item-list-renderer { 321 | margin-bottom: 20px; 322 | } 323 | 324 | yt-live-chat-text-message-renderer { 325 | font-size: 24px !important; 326 | line-height: 32px; 327 | } 328 | 329 | 330 | yt-live-chat-text-message-renderer, 331 | yt-live-chat-paid-message-renderer, 332 | yt-live-chat-membership-item-renderer { 333 | cursor: pointer; 334 | } 335 | 336 | yt-live-chat-text-message-renderer[is-deleted], 337 | yt-live-chat-paid-message-renderer[is-deleted], 338 | yt-live-chat-membership-item-renderer[is-deleted] { 339 | cursor: default; 340 | } 341 | 342 | 343 | 344 | 345 | yt-live-chat-viewer-engagement-message-renderer { 346 | display: none !important; 347 | } 348 | 349 | 350 | #featured { 351 | position: absolute; 352 | z-index: 1000; 353 | top: 8px; 354 | right: 50px; 355 | } 356 | #featured img { 357 | vertical-align: middle; 358 | } 359 | 360 | 361 | .button { 362 | text-decoration: none; 363 | padding: 6px; 364 | border: 1px #bbb solid; 365 | border-radius: 4px; 366 | color: #fff; 367 | background: #444; 368 | } 369 | #pop-out-button { 370 | margin-left: 20px; 371 | } 372 | #pop-out-url { 373 | margin-left: 20px; 374 | font-size: 1.2em; 375 | width: 400px; 376 | border-radius: 4px; 377 | } 378 | 379 | .hidden { 380 | display: none; 381 | } 382 | 383 | yt-live-chat-app tp-yt-iron-dropdown { 384 | display: none; 385 | } 386 | yt-live-chat-app yt-sort-filter-sub-menu-renderer tp-yt-iron-dropdown { 387 | display: block; 388 | } 389 | 390 | -------------------------------------------------------------------------------- /youtube.js: -------------------------------------------------------------------------------- 1 | var showOnlyFirstName; 2 | 3 | var highlightWords = []; 4 | var usePersistentSessionID = false; 5 | var sessionID = ""; 6 | var remoteWindowURL = "https://chat.aaronpk.tv/overlay/"; 7 | var remoteServerURL = "https://chat.aaronpk.tv/overlay/pub"; 8 | var version = "0.3.9"; 9 | var config = {}; 10 | var lastID = ""; 11 | var videoID = ""; 12 | var autoHideTimer = null; 13 | 14 | $("body").unbind("click").on("click", "yt-live-chat-text-message-renderer,yt-live-chat-paid-message-renderer,yt-live-chat-membership-item-renderer,ytd-sponsorships-live-chat-gift-purchase-announcement-renderer,yt-live-chat-paid-sticker-renderer", function() { 15 | 16 | $(".active-comment").removeClass("active-comment"); 17 | 18 | // "Click" on some innocuous part of the page to hide the moderation popup thingy. 19 | // YouTube seems to want to pop that up any time you click anywhere on a message. 20 | setTimeout(function(){ 21 | $("yt-live-chat-message-input-renderer").click(); 22 | }, 200); 23 | 24 | clearTimeout(autoHideTimer); 25 | 26 | // Don't show deleted messages 27 | if($(this)[0].hasAttribute("is-deleted")) { 28 | console.log("Not showing deleted message"); 29 | return; 30 | } 31 | 32 | var data = {}; 33 | 34 | $(".hl-c-cont").remove(); 35 | 36 | data.chatId = $(this).attr("id"); 37 | 38 | if(data.chatId === lastID) { 39 | hideActiveChat(); 40 | return; 41 | } 42 | 43 | data.authorname = $(this).find("#author-name").text(); 44 | if(showOnlyFirstName) { 45 | data.authorname = data.authorname.replace(/ [^ ]+$/, ''); 46 | } 47 | data.authorimg = $(this).find("#img").attr("src"); 48 | // Replace the 32px and 64px avatar with a 128px avatar but keep the identifier identical before the first '=' 49 | const equalIndex = data.authorimg.indexOf("="); 50 | if (equalIndex !== -1) { 51 | let part1 = data.authorimg.slice(0, equalIndex); 52 | let part2 = data.authorimg.slice(equalIndex); 53 | part2 = part2.replace("s32", "s128").replace("s64", "s128"); 54 | 55 | data.authorimg = part1 + part2; 56 | } 57 | 58 | data.message = $(this).find("#message").html(); 59 | 60 | data.sticker = $(this).find(".yt-live-chat-paid-sticker-renderer #sticker #img").attr("src"); 61 | 62 | 63 | // Donation amounts for stickers use a different id than regular superchats 64 | if(data.sticker) { 65 | data.donation = $(this).find("#purchase-amount-chip").html(); 66 | } else { 67 | data.donation = $(this).find("#purchase-amount .yt-live-chat-paid-message-renderer").html(); 68 | } 69 | 70 | data.badges = ""; 71 | if($(this).find("#chat-badges .yt-live-chat-author-badge-renderer img").length > 0) { 72 | data.badges = $(this).find("#chat-badges .yt-live-chat-author-badge-renderer img").parent().html(); 73 | } 74 | 75 | // Mark this comment as shown 76 | $(this).addClass("shown-comment").addClass("active-comment"); 77 | 78 | data.donationHTML = ''; 79 | if(data.donation) { 80 | data.donationHTML = '
' + data.donation + '
'; 81 | } 82 | 83 | data.membership = $(this).find(".yt-live-chat-membership-item-renderer #header-subtext").html(); // membership level e.g. "SILVER" 84 | data.giftedMembership = $(this).find(".ytd-sponsorships-live-chat-header-renderer #primary-text").html(); // Bob gifted 20 memberships 85 | 86 | data.membershipHTML = ''; 87 | 88 | // Try to find the membership level name 89 | data.membershipLevel = ''; 90 | if(data.membership) { 91 | var membershipLevelName; 92 | if(m=data.membership.match(/(Welcome|Upgraded membership) to (.+)!/)) { 93 | membershipLevelName = m[2]; 94 | } else { 95 | membershipLevelName = data.membership; 96 | } 97 | switch(membershipLevelName) { 98 | case 'SILVER': 99 | data.membershipLevel = 'silver'; break; 100 | case 'GOLD': 101 | data.membershipLevel = 'gold'; break; 102 | case 'PLATINUM': 103 | data.membershipLevel = 'platinum'; break; 104 | case 'DIAMOND': 105 | data.membershipLevel = 'diamond'; break; 106 | case 'EMERALD': 107 | data.membershipLevel = 'emerald'; break; 108 | } 109 | } 110 | 111 | 112 | if(data.giftedMembership) { 113 | data.membershipHTML = '
GIFT
'; 114 | data.message = data.giftedMembership; 115 | } else if(data.membership) { 116 | if(data.message) { 117 | data.membershipLength = $(this).find(".yt-live-chat-membership-item-renderer #header-primary-text").text(); // "Member for 20 months" 118 | if(data.membershipLength) { 119 | if(m = data.membershipLength.match(/Member for (.+)/)) { 120 | data.membership = data.membership + '
'+m[1]+''; 121 | } 122 | } 123 | // Member chat, show their member tier under their photo. Message will have been extracted already. 124 | data.membershipHTML = '
'+data.membership+'
'; 125 | } else { 126 | // New member or upgrade, show the tier in the main message section 127 | if(data.membership.match(/Upgraded membership/)) { 128 | data.membershipHTML = '
UPGRADE
'; 129 | } else { 130 | data.membershipHTML = '
NEW
MEMBER!
'; 131 | } 132 | data.message = data.membership; 133 | } 134 | } 135 | 136 | 137 | if(data.sticker) { 138 | data.message = ''; 139 | } 140 | 141 | data.backgroundColor = ""; 142 | data.textColor = ""; 143 | if(this.style.getPropertyValue('--yt-live-chat-paid-message-primary-color')) { 144 | data.backgroundColor = "background-color: "+this.style.getPropertyValue('--yt-live-chat-paid-message-primary-color')+";"; 145 | data.textColor = "color: #111;"; 146 | } 147 | 148 | // This doesn't work yet 149 | // if(this.style.getPropertyValue('--yt-live-chat-sponsor-color')) { 150 | // data.backgroundColor = "background-color: "+this.style.getPropertyValue('--yt-live-chat-sponsor-color')+";"; 151 | // data.textColor = "color: #111;"; 152 | // } 153 | 154 | // console.log(data); 155 | 156 | var html = '
' 157 | + '
' + data.authorname 158 | + '
' + data.badges + '
' 159 | + '
' 160 | + '
' + data.message + '
' 161 | + '
' 162 | +data.donationHTML+data.membershipHTML 163 | +'
'; 164 | 165 | lastID = data.chatId; 166 | 167 | if(sessionID) { 168 | 169 | var remote = { 170 | version: version, 171 | command: "show", 172 | html: html, 173 | config: config, 174 | v: videoID 175 | } 176 | $.post(remoteServerURL+"?v="+videoID+"&id="+sessionID, JSON.stringify(remote)); 177 | 178 | } else { 179 | 180 | $( "highlight-chat" ).removeClass("preview").append(html) 181 | .delay(10).queue(function(next){ 182 | $( ".hl-c-cont" ).removeClass("fadeout"); 183 | next(); 184 | }); 185 | 186 | } 187 | 188 | if(config.autoHideSeconds && config.autoHideSeconds > 0) { 189 | autoHideTimer = setTimeout(function(){ 190 | hideActiveChat(); 191 | }, config.autoHideSeconds*1000); 192 | } 193 | 194 | }); 195 | 196 | function hideActiveChat() { 197 | if(sessionID) { 198 | var remote = { 199 | version: version, 200 | command: "hide", 201 | config: config, 202 | v: videoID 203 | }; 204 | $.post(remoteServerURL+"?v="+videoID+"&id="+sessionID, JSON.stringify(remote)); 205 | } 206 | 207 | $(".hl-c-cont").addClass("fadeout").delay(300).queue(function(){ 208 | $(".hl-c-cont").remove().dequeue(); 209 | }); 210 | 211 | lastID = false; 212 | } 213 | 214 | $("body").on("click", ".btn-clear", function() { 215 | hideActiveChat(); 216 | }); 217 | 218 | $("yt-live-chat-app").before( '' ); 219 | $("body").addClass("inline-chat"); 220 | 221 | // Restore settings 222 | var configProperties = ["color","scale","sizeOffset", 223 | "commentBottom","commentHeight","authorBackgroundColor", 224 | "authorAvatarBorderColor","authorColor","commentBackgroundColor","commentColor", 225 | "fontFamily","showOnlyFirstName","highlightWords", 226 | "popoutURL","serverURL","autoHideSeconds", 227 | "authorAvatarOverlayOpacity","persistentSessionID","sessionID" 228 | ]; 229 | chrome.storage.sync.get(configProperties, function(item){ 230 | var color = "#000"; 231 | if(item.color) { 232 | color = item.color; 233 | } 234 | 235 | let root = document.documentElement; 236 | root.style.setProperty("--keyer-bg-color", color); 237 | 238 | if(item.authorBackgroundColor) { 239 | root.style.setProperty("--author-bg-color", item.authorBackgroundColor); 240 | root.style.setProperty("--author-avatar-border-color", item.authorBackgroundColor); 241 | } 242 | if(item.authorAvatarBorderColor) { 243 | root.style.setProperty("--author-avatar-border-color", item.authorAvatarBorderColor); 244 | } 245 | if(item.authorAvatarOverlayOpacity) { 246 | root.style.setProperty("--author-avatar-overlay-opacity", item.authorAvatarOverlayOpacity); 247 | } 248 | if(item.commentBackgroundColor) { 249 | root.style.setProperty("--comment-bg-color", item.commentBackgroundColor); 250 | } 251 | if(item.authorColor) { 252 | root.style.setProperty("--author-color", item.authorColor); 253 | } 254 | if(item.commentColor) { 255 | root.style.setProperty("--comment-color", item.commentColor); 256 | } 257 | if(item.fontFamily) { 258 | root.style.setProperty("--font-family", item.fontFamily); 259 | } 260 | if(item.scale) { 261 | root.style.setProperty("--comment-scale", item.scale); 262 | } 263 | if(item.commentBottom) { 264 | root.style.setProperty("--comment-area-bottom", item.commentBottom); 265 | } 266 | if(item.commentHeight) { 267 | root.style.setProperty("--comment-area-height", item.commentHeight); 268 | } 269 | if(item.sizeOffset) { 270 | root.style.setProperty("--comment-area-size-offset", item.sizeOffset); 271 | } 272 | showOnlyFirstName = item.showOnlyFirstName; 273 | highlightWords = item.highlightWords; 274 | 275 | if(item.popoutURL) { 276 | remoteWindowURL = item.popoutURL; 277 | } 278 | if(item.serverURL) { 279 | remoteServerURL = item.serverURL; 280 | } 281 | 282 | if(item.persistentSessionID && item.sessionID) { 283 | usePersistentSessionID = item.sessionID; 284 | } 285 | 286 | config = item; 287 | }); 288 | 289 | 290 | // $("#primary-content").append('Aspect Ratio: '); 291 | $("#primary-content").append('Get Overlay URL'); 292 | $("#primary-content").append(''); 293 | 294 | function displayAspectRatio() { 295 | var ratio = Math.round(window.innerWidth / window.innerHeight * 100) / 100; 296 | ratio += " (target 1.77)"; 297 | $("#aspect-ratio").text(ratio); 298 | } 299 | // displayAspectRatio(); 300 | // window.onresize = displayAspectRatio; 301 | 302 | $("#pop-out-button").click(function(e){ 303 | e.preventDefault(); 304 | 305 | if(usePersistentSessionID) { 306 | sessionID = usePersistentSessionID; 307 | } 308 | 309 | if(!sessionID) { 310 | if(window.location.hash) { 311 | sessionID = window.location.hash.replace("#", ""); 312 | } else { 313 | sessionID = generateSessionID(); 314 | } 315 | } 316 | 317 | window.location.hash = sessionID; 318 | 319 | $("#pop-out-url").val(remoteWindowURL+"#"+sessionID); 320 | $("#pop-out-url").parent().removeClass("hidden"); 321 | 322 | $("highlight-chat").remove(); 323 | $("body").removeClass("inline-chat"); 324 | $("#aspect-ratio-container").addClass("hidden"); 325 | $("#get-overlay-url-container").addClass("hidden"); 326 | }); 327 | 328 | $("#pop-out-url").click(function(){ 329 | $(this).select(); 330 | }); 331 | 332 | $(document).keyup(function(e){ 333 | 334 | // Escape key hides active chat 335 | if(e.keyCode === 27) { 336 | hideActiveChat(); 337 | } 338 | 339 | }); 340 | 341 | $(function(){ 342 | 343 | // Show a placeholder message so you can position the window before the chat is live 344 | var data = {}; 345 | data.message = "this livestream is the best!"; 346 | data.authorimg = remoteWindowURL+"/youtube-live-chat-sample-avatar.png"; 347 | $( "highlight-chat" ).addClass("preview").append('
Sample User
' + data.message + '
') 348 | .delay(10).queue(function(next){ 349 | $( ".hl-c-cont" ).removeClass("fadeout"); 350 | next(); 351 | }); 352 | 353 | // Restore the popout URL field if they refresh the page 354 | if(window.location.hash) { 355 | $("#pop-out-button").click(); 356 | } 357 | 358 | // Show banner 359 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 360 | 361 | const params = new URLSearchParams(window.location.search); 362 | videoID = params.get('v'); 363 | 364 | $.post("https://chat.aaronpk.tv/featured.php", { 365 | lang: window.navigator.language, 366 | tz: timezone, 367 | version: version, 368 | v: videoID 369 | }, function(response){ 370 | if(response && response.img) { 371 | var link = 'https://chat.aaronpk.tv/redirect.php?tag='+response.tag+'&lang='+window.navigator.language+'&tz='+timezone+"&version="+version; 372 | $("body").append('