Skip to content

Instantly share code, notes, and snippets.

@funmaker
Created May 27, 2021 09:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save funmaker/d83d7b9ce911a7e20ce1e65bb73f56dd to your computer and use it in GitHub Desktop.
Save funmaker/d83d7b9ce911a7e20ce1e65bb73f56dd to your computer and use it in GitHub Desktop.
YT 24/7 Radio Script
import path from "path";
import fs from "fs";
import fsPromise from "fs/promises";
import cp from "child_process";
import { fileURLToPath } from 'url';
import lame from "@suldashi/lame";
import Canvas from "canvas";
// HOW TO:
// 1) choose streaming target, eg yt
const STREAMING_TARGET = "rtmp://a.rtmp.youtube.com/live2/<YOUTUBE STREAMING KEY>";
// 2) save https://puu.sh/HK4IB/c39ae67267.png as overlay_template.png
// 3) prepare some loop.mp4 video for background loop
// 4) put mp3s in playlist folder
// 5) (optional) acquire Trebucbd.ttf and uncomment line related to it
// 6) npm i --save @suldashi/lame canvas
// 7) node server.js
let overlayTitle = "";
let overlayDesc = "";
let overlayRefreshing = false;
// Uncomment to register bundled font
//Canvas.registerFont("Trebucbd.ttf", { family: "Trebuchet MS" })
const overlay = await Canvas.loadImage('overlay_template.png');
await fsPromise.copyFile("overlay_template.png", "overlay.png");
const canvas = Canvas.createCanvas(overlay.width, overlay.height);
const ctx = canvas.getContext("2d")
async function refreshOverlay() {
if(overlayRefreshing) return;
overlayRefreshing = true;
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(overlay, 0, 0);
ctx.font = 'bold 80px Trebuchet MS';
ctx.fillStyle = "white";
ctx.fillText(overlayTitle.toUpperCase(), 377, 945);
ctx.save();
ctx.rect(353,975,1337, 64);
ctx.clip();
ctx.font = 'bold 40px Trebuchet MS'
ctx.fillText(overlayDesc.toUpperCase(), 365, 1023);
ctx.restore();
const date = new Date();
const time = date.getHours().toString().padStart(2, "0") + ":" + date.getMinutes().toString().padStart(2, "0");
ctx.font = 'bold 40px Trebuchet MS'
ctx.fillText(time, 1737, 1023);
const pngStream = canvas.createPNGStream();
const outFile = fs.createWriteStream("overlay.png.tmp");
pngStream.pipe(outFile);
await new Promise(res => outFile.once("finish", res));
await fsPromise.rename("overlay.png.tmp", "overlay.png");
overlayRefreshing = false;
}
setTimeout(() => {
refreshOverlay();
setInterval(refreshOverlay, 60000)
}, (61 - new Date().getSeconds()) * 1000);
function shuffle(array) {
for(let pos = 0; pos < array.length; pos++) {
const other = Math.floor(Math.random() * (pos + 1));
[array[pos], array[other]] = [array[other], array[pos]];
}
}
const root = path.dirname(fileURLToPath(import.meta.url));
const playlistDir = path.join(root, "playlist");
const playlist = await fsPromise.readdir(playlistDir);
const ffmpeg = cp.spawn(
"ffmpeg",
(
'-threads 4 -y ' +
'-re -f s16le -ar 44100 -ac 2 -i pipe:0 ' +
'-stream_loop -1 -i loop.mp4 ' +
'-loop 1 -f image2 -i overlay.png ' +
'-c:v libx264 -c:a aac ' +
'-b:v 2M -b:a 128k -preset veryfast -maxrate 4M -bufsize 20M -g 60 ' +
'-filter_complex [1][2]overlay[v] ' +
'-map 0:a -map [v] -f flv -flvflags no_duration_filesize ' + STREAMING_TARGET
).split(" "),
{
stdio: ['pipe', process.stdout, process.stderr],
},
);
ffmpeg.once("exit", () => process.exit())
process.on("exit", () => ffmpeg.kill());
while(true) {
console.error("Shuffling the playlist.");
shuffle(playlist);
for(let id in playlist) {
const song = playlist[id];
let basename = song.slice(0, -4);
console.error(`Now playing: ${basename}...`);
const parts = basename.split(" - ");
overlayTitle = parts.slice(1).join(" - ");
const meme = [
"**** ***",
"LIVE 24/7",
"Szkoda strzępić ryja",
"Wy kurwa polacy",
"Coś się zepsuło",
"Nie było mnie słychać",
"To powtorzę jeszcze raz",
"Cała prawda, całą dobę. ▪ Cała prawda, całą dobę.",
];
const mem = meme[Math.floor(Math.random() * meme.length)]
const next = playlist[(id + 1) % playlist.length].slice(0, -4);
overlayDesc = `Autor: ${parts[0]} ▪ ${mem} ▪ Następne: ${next}`;
const file = fs.createReadStream(path.join(playlistDir, song));
const decoder = lame.Decoder({
channels: 2,
bitDepth: 16,
sampleRate: 44100,
mode: lame.STEREO
});
file.pipe(decoder);
decoder.pipe(ffmpeg.stdin, { end: false });
await Promise.all([
refreshOverlay(),
new Promise((res, rej) => {
file.on("error", rej);
decoder.on("error", rej);
decoder.on("end", res);
}),
]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment