Skip to content

Instantly share code, notes, and snippets.

@santaklouse
Created June 3, 2026 14:30
Show Gist options
  • Select an option

  • Save santaklouse/c4d252feb1004ebcce4e414fa4ce0a23 to your computer and use it in GitHub Desktop.

Select an option

Save santaklouse/c4d252feb1004ebcce4e414fa4ce0a23 to your computer and use it in GitHub Desktop.
It takes PlayerJS without Chromecast (pro module), removes the external Dean Edwards Packer layer if necessary inserts the ChromeCast module and adds a button with linkurl:"chromecast" to the skin/template.
#!/usr/bin/env node
'use strict';
/**
* add_playerjs_chromecast.cjs
*
* Uses PlayerJS without Chromecast, removes the external Dean Edwards Packer layer if necessary,
* inserts the ChromeCast module and adds a button with linkurl:"chromecast" to the skin/template.
*
* Usage:
* node add_playerjs_chromecast.cjs ./playerjs.js ./playerjs.with_chromecast.unpacked.js
*
* The output is an unpacked JS file that can be included in the same way as the original.
* For example, new Playerjs({ id: "player", file: "https://example.com/video.mp4", chromecast: { on: 1, receiver: 0, sub: 1, message: 1 }});
*/
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const CHROMECAST_MODULE = 'var ChromeCast=function(){var player;var controller;var deviceName;var words;var current_color;var urls;if(window[\'__onGCastApiAvailable\']){var j=0;for(var i=0;i<window.pljssglobal.length;i++){if(window.pljssglobal[i]){j++}}j==1?CastInit():\'\'}else{window[\'__onGCastApiAvailable\']=function(x,y){if(x){for(var i=0;i<window.pljssglobal.length;i++){if(window.pljssglobal[i]){if(window.pljssglobal[i].api("id")==options.id){window.pljssglobal[i].api(\'castinit\')}}}}else{_log("error: "+y)}}}this.init=function(){CastInit()};function CastInit(){_log("available");if(exist(chrome)){if(exist(chrome.cast)&&exist(cast)&&!o.cast_available){var rid=\'CC1AD845\';if(v.chromecast.receiver==1&&v.chromecast.receiverid){rid=v.chromecast.receiverid}try{cast.framework.CastContext.getInstance().setOptions({receiverApplicationId:rid,autoJoinPolicy:\'tab_and_origin_scoped\',language:\'en-US\',resumeSavedSession:false,androidReceiverCompatible:true});player=new cast.framework.RemotePlayer()}catch(e){log(e)}Controller();o.cast_available=true;setTimeout(Available,1000)}}}function Available(){if(o.controls){o.controls.Review();o.controls.resize()}}function Connection(data){if(cast&&cast.framework){_log((!player.isConnected?\'dis\':\'\')+"connected "+v.id);if(player.isConnected){PlayRemote()}else{PlayLocal()}}}function PlayRemote(x,y){var session=cast.framework.CastContext.getInstance().getCurrentSession();var mime=\'video/mp4\';o.file_type=="hls"?mime="application/x-mpegurl":\'\';o.file_type=="dash"?mime="application/dash+xml":\'\';var url;if(y){url=y}else{url=o.media.currentFile();if(exist(v.casturl)){url=v.casturl}if(exist(o.casturl)){url=o.casturl}url=fjs(url);if(url.indexOf("#"+v.enc2)==0){url=o[o.fd[0]](url)}if(url){if(url.indexOf("#"+v.enc3)==0&&url.indexOf(v.file3_separator)>0){url=o[o.fd[1]](url)}}url=fjs(url);urls=[];if(url.indexOf(":or:")>0){iurl=0;urls=url.split(":or:");url=urls[0]}}var info=new chrome.cast.media.MediaInfo(url,mime);info.metadata=new chrome.cast.media.GenericMediaMetadata();info.metadata.metadataType=chrome.cast.media.MetadataType.GENERIC;var tracks=[];if(exist(o.subs)&&exist(o.current_subtitle)&&v.chromecast.sub==1){info.textTrackStyle=SubStyle();for(var i in o.subs){if(o.subs[i].indexOf(\'vtt\')>0){var t=new chrome.cast.media.Track(0,chrome.cast.media.TrackType.TEXT);t.trackContentId=o.subs[i];t.trackContentType=\'text/vtt\';t.subtype=\'CAPTIONS\';t.name=o.files_subtitle[i];t.trackId=parseInt(i);t.customData=null;tracks.push(t)}}if(tracks.length>0){info.tracks=tracks}}if(v.poster){info.metadata.images=[new chrome.cast.Image(v.poster)]}info.metadata.title=o.titlestore?o.titlestore:(v.title?v.title:\'\');if(v.castname){info.metadata.title=v.castname}var request=new chrome.cast.media.LoadRequest(info);request.currentTime=o.seekto>0?o.seekto:(o.casting?0:o.media.time());request.autoplay=o.play||x==true;if(tracks.length>0&&o.current_subtitle>-1){if(o.current_subtitle<tracks.length){request.activeTrackIds=[parseInt(o.current_subtitle)];_log(\'subtitle \'+o.current_subtitle)}}if(session){session.loadMedia(request).then(function(){deviceName=session.getCastDevice().friendlyName;_log("connected to "+deviceName);if(v.chromecast.message!=0&&!o.casting){words?o.frame.removeChild(words):\'\';words=createElement("div");css(words,{"position":"absolute","top":"20px","width":"100%","left":0,"opacity":0.7,"color":"#fff","pointer-events":"none"});words.innerHTML=\'<center>\'+Lang("castdevice")+\' &laquo;\'+deviceName+\'&raquo;</center>\';o.frame.appendChild(words)}player.volumeLevel=v.volume;controller.setVolumeLevel();if(o.muted&&!player.isMuted){controller.muteOrUnmute()}js("casted");if(x!=true){if(!o.play){!player.isPaused?controller.playOrPause():\'\'}else{o.media.Pause()}}hide(o.mediacontainer);o.casting=true},function(e){_log(e);ErrLocal()})}else{_log("no session");ErrLocal()}}function ErrLocal(){var x;if(urls.length>1){if(iurl<urls.length-1){iurl++;x=true;o.media.Pause();PlayRemote(true,urls[iurl]);setTimeout(function(){o.controls.Play()},300)}}if(!x){PlayLocal();o.alert.txt(Lang("casterror"))}}function PlayLocal(){words?o.frame.removeChild(words):\'\';words=undefined;o.casting=false;show(o.mediacontainer);js("uncasted");if(player){if(player.savedPlayerState){o.actions.Seek(player.savedPlayerState.currentTime);if(player.savedPlayerState.isPaused){o.media.Pause()}else{o.media.Play()}}}}function PlayChanged(){if(player.isPaused){_log("pause");js("castpause");o.controls.Pause()}else{_log("play");js("castplay");o.controls.Play()}}function VolumeChanged(){o.actions.Volume(player.volumeLevel)}function MuteChanged(){player.isMuted?o.actions.Mute():o.actions.Unmute()}function StateChanged(){if(player.isConnected){if(player.playerState==null){if(player.currentTime==player.duration){o.controls.Pause();o.media.onEnded()}}}}function Controller(){_log("control "+v.id);controller=new cast.framework.RemotePlayerController(player);controller.addEventListener(cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,Connection);controller.addEventListener(cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,PlayChanged);controller.addEventListener(cast.framework.RemotePlayerEventType.VOLUME_LEVEL_CHANGED,VolumeChanged);controller.addEventListener(cast.framework.RemotePlayerEventType.IS_MUTED_CHANGED,MuteChanged);controller.addEventListener(cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED,StateChanged)}Script(\'gstatic.com/cv\',\'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1\');function _log(x){if(v.chromecast&&v.log==1){log("chromecast",x)}}this.button=function(x){current_color=x;return"<button is=\'google-cast-button\' id=\'pjs_cast_button_"+v.id+"\' style=\'padding:0;width:20px;height:20px;--connected-color:"+x+";--disconnected-color:"+x+";border:0;background:transparent;pointer-events:auto;cursor:pointer\'></button>"};this.Color=function(x,y){if(current_color!=y){var el=document.getElementById("pjs_cast_button_"+v.id);if(el){var style=el.getAttribute("style");var clr=new RegExp(current_color,"gi");style=style.replace(clr,y);el.setAttribute("style",style)}current_color=y}};this.Volume=function(x){if(player.isConnected){player.volumeLevel=x;controller.setVolumeLevel()}};this.Mute=function(){if(player.isConnected){if(!player.isMuted){controller.muteOrUnmute()}}};this.Unmute=function(){if(player.isConnected){if(player.isMuted){controller.muteOrUnmute()}}};this.Play=function(x){if(player.isConnected){if(player.isPaused){controller.playOrPause()}else{if(player.playerState==null){}}}};this.Pause=function(x){if(player.isConnected&&!player.isPaused){controller.playOrPause()}};this.Sub=function(){if(v.chromecast.sub==1){UpdateSub()}};function fjs(x){if(x.indexOf(\'js:\')==0){try{x=eval(x.substr(3))}catch(e){console.log(e.message)}}return x?x:\'\'}function UpdateSub(){if(player.isConnected){var ss=SubStyle();var request=new chrome.cast.media.EditTracksInfoRequest([parseInt(o.current_subtitle)],ss);cast.framework.CastContext.getInstance().getCurrentSession().getSessionObj().media[0].editTracksInfo(request,function(){_log(\'subtitle \'+o.current_subtitle)},function(e){_log(\'subtitle error\'+e)})}}function SubStyle(){var x=new chrome.cast.media.TextTrackStyle();var bga=Math.round(v.sub_bga*255).toString(16);x.backgroundColor=CheckColor(v.sub_bgcolor)+(bga.length==1?bga+\'0\':bga);x.edgeColor=\'#00000016\';x.edgeType=\'DROP_SHADOW\';x.fontFamily=\'CASUAL\';x.fontScale=parseFloat(parseInt(v.sub_sizeproc)/100);x.foregroundColor=CheckColor(v.sub_color)+Math.round(255).toString(16);return x};this.Time=function(x){var x;if(player.isConnected){x=player.currentTime}return x};this.Duration=function(x){var x;if(player.isConnected){x=player.duration}return x};this.Exit=function(){Exit()};function Exit(){if(cast&&cast.framework){PlayLocal()}};this.Go=function(){if(cast&&cast.framework){if(player.isConnected){PlayRemote(o.play)}}};this.Seek=function(x){if(player.isConnected){player.currentTime=x;controller.seek()}};this.remove=function(){Remove()};function Remove(){Exit();if(controller){controller.removeEventListener(cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,Connection);controller.removeEventListener(cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,PlayChanged);controller.removeEventListener(cast.framework.RemotePlayerEventType.VOLUME_LEVEL_CHANGED,VolumeChanged);controller.removeEventListener(cast.framework.RemotePlayerEventType.IS_MUTED_CHANGED,MuteChanged);controller.removeEventListener(cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED,StateChanged);cast.framework.CastContext.getInstance().endCurrentSession();controller=null}}};';
const CHROMECAST_BUTTON_SNIPPET = `
/* === PlayerJS Chromecast skin button injected === */
if(v.chromecast&&v.chromecast.on==1&&!v.control_chromecast){v.control_chromecast={order:13,on:1,icon:"<svg width='20' height='20' viewBox='0 0 24 24'><g fill='none' fill-rule='evenodd'><path d='M3 18v3h3c0-1.657-1.343-3-3-3zm0-4v2c2.761 0 5 2.239 5 5h2c0-3.866-3.134-7-7-7zm0-4v2c4.971 0 9 4.029 9 9h2c0-6.075-4.925-11-11-11zm0-7h18v14h-5.05c-.236-.695-.548-1.355-.928-1.97H19V5H5v3.078A13.91 13.91 0 0 0 3 8V3z' fill='#ffffff'/></g></svg>",type:"svg",position:"controls",margin:"0 0 0 15",link:1,linkurl:"chromecast",copy:1,iconscolor:"ffffff",click:1,tip:0,hide:0,scale:"1",scaleover:"-1"}}
/* === /PlayerJS Chromecast skin button injected === */
`;
function usageAndExit() {
console.error(`Usage:\n node ${path.basename(process.argv[1])} <input-playerjs.js> <output-playerjs.with_chromecast.unpacked.js>`);
process.exit(1);
}
function readText(filePath) {
if (!filePath || !fs.existsSync(filePath)) {
throw new Error(`Файл не найден: ${filePath}`);
}
return fs.readFileSync(filePath, 'utf8');
}
function looksPacked(code) {
return /eval\s*\(\s*function\s*\(\s*p\s*,\s*a\s*,\s*c\s*,\s*k\s*,\s*e\s*,\s*d\s*\)/.test(code);
}
function unpackDeanEdwardsPacker(code) {
if (!looksPacked(code)) return code;
let unpacked = '';
const context = {
eval(innerCode) {
unpacked = String(innerCode);
return undefined;
},
console,
window: {},
document: {},
navigator: {},
location: {},
Image: function Image() {},
setTimeout() {},
clearTimeout() {},
setInterval() {},
clearInterval() {},
};
vm.createContext(context);
vm.runInContext(code, context, { timeout: 15000 });
if (!unpacked || !unpacked.includes('function Playerjs(options)')) {
throw new Error('Не удалось распаковать PlayerJS: внешний eval/packer слой не вернул код Playerjs.');
}
return unpacked;
}
function assertPlayerJs(code) {
if (!code.includes('function Playerjs(options)')) {
throw new Error('Это не похоже на распакованный PlayerJS: не найдено function Playerjs(options).');
}
}
function findChromecastHook(code) {
const exact = "if(v.chromecast){v.chromecast.on==1?o.chromecast=new ChromeCast():''}";
const exactPos = code.indexOf(exact);
if (exactPos !== -1) return { index: exactPos, text: exact };
const regex = /if\s*\(\s*v\.chromecast\s*\)\s*\{\s*v\.chromecast\.on\s*==\s*1\s*\?\s*o\.chromecast\s*=\s*new\s+ChromeCast\s*\(\s*\)\s*:\s*''\s*\}/;
const match = code.match(regex);
if (match && typeof match.index === 'number') return { index: match.index, text: match[0] };
throw new Error("Не найден hook Chromecast: if(v.chromecast){v.chromecast.on==1?o.chromecast=new ChromeCast():''}");
}
function injectChromeCastModule(code) {
if (code.includes('var ChromeCast=function()') || code.includes('var ChromeCast = function()')) {
return { code, changed: false };
}
const hook = findChromecastHook(code);
const injected =
code.slice(0, hook.index) +
'\n\n/* === PlayerJS ChromeCast module injected === */\n' +
CHROMECAST_MODULE.trim() +
'\n/* === /PlayerJS ChromeCast module injected === */\n\n' +
code.slice(hook.index);
return { code: injected, changed: true };
}
function injectChromeCastButton(code) {
if (code.includes('linkurl:"chromecast"') || code.includes('linkurl: "chromecast"') || code.includes("linkurl:'chromecast'")) {
return { code, changed: false };
}
const hook = findChromecastHook(code);
const injected = code.slice(0, hook.index) + CHROMECAST_BUTTON_SNIPPET + code.slice(hook.index);
return { code: injected, changed: true };
}
function syntaxCheck(code, outputFile) {
try {
new vm.Script(code, { filename: outputFile });
} catch (error) {
throw new Error(`После вставки JS не проходит syntax check: ${error.message}`);
}
}
function main() {
const inputFile = process.argv[2];
const outputFile = process.argv[3] || 'playerjs.with_chromecast.unpacked.js';
if (!inputFile) usageAndExit();
const inputCode = readText(inputFile);
let code = unpackDeanEdwardsPacker(inputCode);
assertPlayerJs(code);
const moduleResult = injectChromeCastModule(code);
code = moduleResult.code;
const buttonResult = injectChromeCastButton(code);
code = buttonResult.code;
syntaxCheck(code, outputFile);
fs.writeFileSync(outputFile, code, 'utf8');
console.log('OK');
console.log(`Input: ${inputFile}`);
console.log(`Output: ${outputFile}`);
console.log(`Unpacked: ${looksPacked(inputCode) ? 'yes' : 'input was already unpacked'}`);
console.log(`ChromeCast module injected: ${moduleResult.changed ? 'yes' : 'already existed'}`);
console.log(`ChromeCast button injected: ${buttonResult.changed ? 'yes' : 'already existed'}`);
console.log(`Output size: ${Buffer.byteLength(code, 'utf8')} bytes`);
}
try {
main();
} catch (error) {
console.error('ERROR:', error.message);
process.exit(1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment