1// Import cast framework 2if (window.chrome && !window.chrome.cast) { 3 var script = document.createElement("script"); 4 script.src = 5 "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"; 6 document.head.appendChild(script); 7} 8 9// Castjs 10class Castjs { 11 // constructor takes optional options 12 constructor(opt = {}) { 13 // valid join policies 14 var joinpolicies = [ 15 "tab_and_origin_scoped", 16 "origin_scoped", 17 "page_scoped", 18 ]; 19 20 // only allow valid join policy 21 if (!opt.joinpolicies || joinpolicies.indexOf(opt.joinpolicy) === -1) { 22 opt.joinpolicy = "tab_and_origin_scoped"; 23 } 24 25 // set default receiver ID if none provided 26 if (!opt.receiver || opt.receiver === "") { 27 opt.receiver = "CC1AD845"; 28 } 29 30 // private variables 31 this._events = {}; 32 this._player = null; 33 this._controller = null; 34 35 // public variables 36 this.version = "v4.1.2"; 37 this.receiver = opt.receiver; 38 this.joinpolicy = opt.joinpolicy; 39 this.available = false; 40 this.connected = false; 41 this.device = "Chromecast"; 42 this.src = ""; 43 this.title = ""; 44 this.description = ""; 45 this.poster = ""; 46 this.subtitles = []; 47 this.volumeLevel = 1; 48 this.muted = false; 49 this.paused = false; 50 this.time = 0; 51 this.timePretty = "00:00:00"; 52 this.duration = 0; 53 this.durationPretty = "00:00:00"; 54 this.progress = 0; 55 this.state = "disconnected"; 56 57 // initialize chromecast framework 58 this._init(); 59 } 60 _getBrowser() { 61 if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) { 62 return "Firefox: Casting is not supported in this browser."; 63 } 64 if (navigator.userAgent.toLowerCase().indexOf("opr/") > -1) { 65 return "Opera: Please enable casting, click here: https://bit.ly/2G1PMhD"; 66 } 67 if (navigator.userAgent.toLowerCase().indexOf("iron safari") > -1) { 68 return "Iron Safari: Please enable casting, click here: https://bit.ly/2G1PMhD"; 69 } 70 } 71 _init(tries = 0) { 72 // casting only works on chrome, opera, brave and vivaldi 73 if ( 74 !window.chrome || 75 !window.chrome.cast || 76 !window.chrome.cast.isAvailable 77 ) { 78 if (tries++ > 20) { 79 return this.trigger( 80 "error", 81 "Casting is not supported in " + getBrowser(), 82 ); 83 } 84 return setTimeout(this._init.bind(this), 250, tries); 85 } 86 87 // terminate loop 88 clearInterval(this.intervalIsAvailable); 89 // initialize cast API 90 cast.framework.CastContext.getInstance().setOptions({ 91 receiverApplicationId: this.receiver, 92 autoJoinPolicy: this.joinpolicy, 93 language: "en-US", 94 resumeSavedSession: false, 95 }); 96 // create remote player controller 97 this._player = new cast.framework.RemotePlayer(); 98 this._controller = new cast.framework.RemotePlayerController(this._player); 99 100 // register callback events 101 this._controller.addEventListener( 102 "isConnectedChanged", 103 this._isConnectedChanged.bind(this), 104 ); 105 this._controller.addEventListener( 106 "isMediaLoadedChanged", 107 this._isMediaLoadedChanged.bind(this), 108 ); 109 this._controller.addEventListener( 110 "isMutedChanged", 111 this._isMutedChanged.bind(this), 112 ); 113 this._controller.addEventListener( 114 "isPausedChanged", 115 this._isPausedChanged.bind(this), 116 ); 117 this._controller.addEventListener( 118 "currentTimeChanged", 119 this._currentTimeChanged.bind(this), 120 ); 121 this._controller.addEventListener( 122 "durationChanged", 123 this._durationChanged.bind(this), 124 ); 125 this._controller.addEventListener( 126 "volumeLevelChanged", 127 this._volumeLevelChanged.bind(this), 128 ); 129 this._controller.addEventListener( 130 "playerStateChanged", 131 this._playerStateChanged.bind(this), 132 ); 133 this.available = true; 134 this.trigger("available"); 135 } 136 137 _isMediaLoadedChanged() { 138 // don't update media info if not available 139 if (!this._player.isMediaLoaded) { 140 return; 141 } 142 // there is a bug where mediaInfo is not directly available 143 // so we are skipping one tick in the event loop, zzzzzzzzz 144 setTimeout(() => { 145 if (!this._player.mediaInfo) { 146 return; 147 } 148 // Update device name 149 this.device = 150 cast.framework.CastContext.getInstance() 151 .getCurrentSession() 152 .getCastDevice().friendlyName || this.device; 153 154 // Update media variables 155 this.src = this._player.mediaInfo.contentId; 156 this.title = this._player.title || null; 157 this.description = this._player.mediaInfo.metadata.subtitle || null; 158 this.poster = this._player.imageUrl || null; 159 this.subtitles = []; 160 this.volumeLevel = this.volumeLevel = Number( 161 this._player.volumeLevel.toFixed(1), 162 ); 163 this.muted = this._player.isMuted; 164 this.paused = this._player.isPaused; 165 this.time = Math.round(this._player.currentTime, 1); 166 this.timePretty = this._controller.getFormattedTime(this.time); 167 this.duration = this._player.duration; 168 this.durationPretty = this._controller.getFormattedTime( 169 this._player.duration, 170 ); 171 this.progress = this._controller.getSeekPosition( 172 this.time, 173 this._player.duration, 174 ); 175 this.state = this._player.playerState.toLowerCase(); 176 177 // Loop over the subtitle tracks 178 for (var i in this._player.mediaInfo.tracks) { 179 // Check for subtitle 180 if (this._player.mediaInfo.tracks[i].type === "TEXT") { 181 // Push to media subtitles array 182 this.subtitles.push({ 183 label: this._player.mediaInfo.tracks[i].name, 184 src: this._player.mediaInfo.tracks[i].trackContentId, 185 }); 186 } 187 } 188 // Get the active subtitle 189 var active = cast.framework.CastContext.getInstance() 190 .getCurrentSession() 191 .getSessionObj().media[0].activeTrackIds; 192 if (active.length && this.subtitles[active[0]]) { 193 this.subtitles[active[0]].active = true; 194 } 195 }); 196 } 197 // Player controller events 198 _isConnectedChanged() { 199 this.connected = this._player.isConnected; 200 if (this.connected) { 201 this.device = 202 cast.framework.CastContext.getInstance() 203 .getCurrentSession() 204 .getCastDevice().friendlyName || this.device; 205 } 206 this.state = !this.connected ? "disconnected" : "connected"; 207 this.trigger("statechange"); 208 this.trigger(!this.connected ? "disconnect" : "connect"); 209 } 210 _currentTimeChanged() { 211 var past = this.time; 212 this.time = Math.round(this._player.currentTime, 1); 213 this.duration = this._player.duration; 214 this.progress = this._controller.getSeekPosition(this.time, this.duration); 215 this.timePretty = this._controller.getFormattedTime(this.time); 216 this.durationPretty = this._controller.getFormattedTime(this.duration); 217 // Only trigger timeupdate if there is a difference 218 if (past != this.time && !this._player.isPaused) { 219 this.trigger("timeupdate"); 220 } 221 } 222 _durationChanged() { 223 this.duration = this._player.duration; 224 } 225 _volumeLevelChanged() { 226 this.volumeLevel = Number(this._player.volumeLevel.toFixed(1)); 227 if (this._player.isMediaLoaded) { 228 this.trigger("volumechange"); 229 } 230 } 231 _isMutedChanged() { 232 var old = this.muted; 233 this.muted = this._player.isMuted; 234 if (old != this.muted) { 235 this.trigger(this.muted ? "mute" : "unmute"); 236 } 237 } 238 _isPausedChanged() { 239 this.paused = this._player.isPaused; 240 if (this.paused) { 241 this.trigger("pause"); 242 } 243 } 244 _playerStateChanged() { 245 this.connected = this._player.isConnected; 246 if (!this.connected) { 247 return; 248 } 249 this.device = 250 cast.framework.CastContext.getInstance() 251 .getCurrentSession() 252 .getCastDevice().friendlyName || this.device; 253 this.state = this._player.playerState.toLowerCase(); 254 switch (this.state) { 255 case "idle": 256 this.state = "ended"; 257 this.trigger("statechange"); 258 this.trigger("end"); 259 return this; 260 case "buffering": 261 this.time = Math.round(this._player.currentTime, 1); 262 this.duration = this._player.duration; 263 this.progress = this._controller.getSeekPosition( 264 this.time, 265 this.duration, 266 ); 267 this.timePretty = this._controller.getFormattedTime(this.time); 268 this.durationPretty = this._controller.getFormattedTime(this.duration); 269 this.trigger("statechange"); 270 this.trigger("buffering"); 271 return this; 272 case "playing": 273 // we have to skip a tick to give mediaInfo some time to update 274 setTimeout(() => { 275 this.trigger("statechange"); 276 this.trigger("playing"); 277 }); 278 return this; 279 } 280 } 281 // Class functions 282 on(event, cb) { 283 // If event is not registered, create array to store callbacks 284 if (!this._events[event]) { 285 this._events[event] = []; 286 } 287 // Push callback into event array 288 this._events[event].push(cb); 289 return this; 290 } 291 off(event) { 292 if (!event) { 293 // if no event name was given, reset all events 294 this._events = {}; 295 } else if (this._events[event]) { 296 // remove all callbacks from event 297 this._events[event] = []; 298 } 299 return this; 300 } 301 trigger(event) { 302 // Slice arguments into array 303 var tail = Array.prototype.slice.call(arguments, 1); 304 // If event exist, call callback with callback data 305 for (var i in this._events[event]) { 306 this._events[event][i].apply(this, tail); 307 } 308 // dont call global event if error 309 if (event === "error") { 310 return this; 311 } 312 // call global event handler if exist 313 for (var i in this._events["event"]) { 314 this._events["event"][i].apply(this, [event]); 315 } 316 return this; 317 } 318 cast(src, metadata = {}, customData = {}) { 319 // We need a source! Don't forget to enable CORS 320 if (!src) {
321 return this.trigger("error", "No media source specified."); 322 } 323 metadata.src = src; 324 // Update media variables with user input 325 for (var key in metadata) { 326 if (metadata.hasOwnProperty(key)) { 327 this[key] = metadata[key]; 328 } 329 } 330 // Time to request a session! 331 cast.framework.CastContext.getInstance() 332 .requestSession() 333 .then( 334 () => { 335 if (!cast.framework.CastContext.getInstance().getCurrentSession()) { 336 return this.trigger( 337 "error", 338 "Could not connect with the cast device", 339 ); 340 } 341 // Create media cast object 342 var mediaInfo = new chrome.cast.media.MediaInfo(this.src); 343 mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); 344 345 // This part is the reason why people love this library <3 346 if (this.subtitles.length) { 347 // I'm using the Netflix subtitle styling 348 // chrome.cast.media.TextTrackFontGenericFamily.CASUAL 349 // chrome.cast.media.TextTrackEdgeType.DROP_SHADOW 350 mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle(); 351 mediaInfo.textTrackStyle.backgroundColor = "#00000000"; 352 mediaInfo.textTrackStyle.edgeColor = "#00000016"; 353 mediaInfo.textTrackStyle.edgeType = "DROP_SHADOW"; 354 mediaInfo.textTrackStyle.fontFamily = "CASUAL"; 355 mediaInfo.textTrackStyle.fontScale = 1.0; 356 mediaInfo.textTrackStyle.foregroundColor = "#FFFFFF"; 357 358 var tracks = []; 359 for (var i in this.subtitles) { 360 // chrome.cast.media.TrackType.TEXT 361 // chrome.cast.media.TextTrackType.CAPTIONS 362 var track = new chrome.cast.media.Track(i, "TEXT"); 363 track.name = this.subtitles[i].label; 364 track.subtype = "CAPTIONS"; 365 track.trackContentId = this.subtitles[i].src; 366 track.trackContentType = "text/vtt"; 367 // This bug made me question life for a while 368 track.trackId = parseInt(i); 369 tracks.push(track); 370 } 371 mediaInfo.tracks = tracks; 372 } 373 // Let's prepare the metadata 374 mediaInfo.metadata.images = [new chrome.cast.Image(this.poster)]; 375 mediaInfo.metadata.title = this.title; 376 mediaInfo.metadata.subtitle = this.description; 377 mediaInfo.customData = customData; 378 // Prepare the actual request 379 var request = new chrome.cast.media.LoadRequest(mediaInfo); 380 // Didn't really test this currenttime thingy, dont forget 381 request.currentTime = this.time; 382 request.autoplay = !this.paused; 383 // If multiple subtitles, use the active: true one 384 if (this.subtitles.length) { 385 for (var i in this.subtitles) { 386 if (this.subtitles[i].active) { 387 request.activeTrackIds = [parseInt(i)]; 388 break; 389 } 390 } 391 } 392 // Here we go! 393 cast.framework.CastContext.getInstance() 394 .getCurrentSession() 395 .loadMedia(request) 396 .then( 397 () => { 398 // Update device name 399 this.device = 400 cast.framework.CastContext.getInstance() 401 .getCurrentSession() 402 .getCastDevice().friendlyName || this.device; 403 return this; 404 }, 405 (err) => { 406 return this.trigger("error", err); 407 }, 408 ); 409 }, 410 (err) => { 411 if (err !== "cancel") { 412 this.trigger("error", err); 413 } 414 return this; 415 }, 416 ); 417 } 418 seek(seconds, isPercentage) { 419 // if seek(15, true) we assume 15 is percentage instead of seconds 420 if (isPercentage) { 421 seconds = this._controller.getSeekTime(seconds, this._player.duration); 422 } 423 this._player.currentTime = seconds; 424 this._controller.seek(); 425 return this; 426 } 427 volume(float) { 428 this._player.volumeLevel = float; 429 this._controller.setVolumeLevel(); 430 return this; 431 } 432 play() { 433 if (this.paused) { 434 this._controller.playOrPause(); 435 } 436 return this; 437 } 438 pause() { 439 if (!this.paused) { 440 this._controller.playOrPause(); 441 } 442 return this; 443 } 444 mute() { 445 if (!this.muted) { 446 this._controller.muteOrUnmute(); 447 } 448 return this; 449 } 450 unmute() { 451 if (this.muted) { 452 this._controller.muteOrUnmute(); 453 } 454 return this; 455 }
456 // subtitle allows you to change active subtitles while casting 457 subtitle(index) { 458 // this is my favorite part of castjs 459 // prepare request to edit the tracks on current session 460 var request = new chrome.cast.media.EditTracksInfoRequest([ 461 parseInt(index), 462 ]); 463 cast.framework.CastContext.getInstance() 464 .getCurrentSession() 465 .getSessionObj() 466 .media[0].editTracksInfo( 467 request, 468 () => { 469 // after updating the device we should update locally 470 // loop trough subtitles 471 for (var i in this.subtitles) { 472 // remove active key from all subtitles 473 delete this.subtitles[i].active; 474 // if subtitle matches given index, we set to true 475 if (i == index) { 476 this.subtitles[i].active = true; 477 } 478 } 479 return this.trigger("subtitlechange"); 480 }, 481 (err) => { 482 // catch any error 483 return this.trigger("error", err); 484 }, 485 ); 486 } 487 // disconnect will end the current session 488 disconnect() { 489 cast.framework.CastContext.getInstance().endCurrentSession(true); 490 this._controller.stop(); 491 492 // application variables 493 this.connected = false; 494 this.device = "Chromecast"; 495 496 // media variables 497 this.src = ""; 498 this.title = ""; 499 this.description = ""; 500 this.poster = ""; 501 this.subtitles = []; 502 503 // player variable 504 this.volumeLevel = 1; 505 this.muted = false; 506 this.paused = false; 507 this.time = 0; 508 this.timePretty = "00:00:00"; 509 this.duration = 0; 510 this.durationPretty = "00:00:00"; 511 this.progress = 0; 512 this.state = "disconnected"; 513 514 this.trigger("disconnect"); 515 return this; 516 } 517} 518 519// Expose Castjs globally for browser usage 520window.Castjs = Castjs; 521 522if (typeof module !== "undefined") { 523 module.exports = Castjs; 524}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.