Skip to content

Instantly share code, notes, and snippets.

@FlyInk13
Last active March 25, 2021 13:29
Show Gist options
  • Save FlyInk13/781f38c954fdec7ed7a8d99585507a8e to your computer and use it in GitHub Desktop.
Save FlyInk13/781f38c954fdec7ed7a8d99585507a8e to your computer and use it in GitHub Desktop.
Отрисовка змейки на node/canvas и стриминг ВКонтакте через ffmpeg. (Для запуска нужен токен VK Live https://vk.cc/6njXbx)
const https = require('https');
const fs = require('fs');
const { spawn } = require('child_process');
const { Image, createCanvas } = require('canvas');
const VK = require('VK-Promise');
const canvas = new createCanvas(1280, 720);
const streamer = VK(""); // https://vk.cc/6njXbx
const request = streamer.request;
const ctx = canvas.getContext('2d');
const fps = 30;
const owner_id = -109837093;
const badDirections = [["ws"], ["sw"], ["ad"], ["da"]];
const backup = {}; // require('./data.json');
let backgroundHue = 0;
let views = '?';
let dir_old = "d";
let snake_upgrade = false;
let lastImage = false;
let apple = backup.apple || [4, 18];
let snake = backup.snake || [[32, 18], [32, 17], [32, 16], [32, 15]];;
let score = backup.score || {};
let last_comment = backup.last_comment || '';
let last_super_comment = backup.last_super_comment || '';
ctx.font = 'bold 14px Roboto,Open Sans';
process.env.TZ = "Europe/Moscow";
process.on("uncaughtException", function(e) {
// Игнорирование ошибок
console.error("uncaughtException", e.stack);
});
Promise.resolve({
name: "Змейка",
wallpost: 1,
}).then((params) => {
if (owner_id < 0) {
params.group_id = -owner_id;
}
return streamer("video.startStreaming", params);
}).then((res) => {
console.log(`Video: https://vk.com/video${res.owner_id}_${res.video_id}`);
streamStart(res.stream);
LongPoll_init(res);
}).catch((error) => {
console.error(error);
});
function saveProgress() {
fs.writeFileSync('./data.json', JSON.stringify({
score,
snake,
apple,
last_comment,
last_super_comment
}));
}
function streamStart({ url, key }) {
console.log('stream_start', url);
const ffmpeg_path = '/usr/bin/ffmpeg'; // which ffmpeg
const ffmpeg = spawn(ffmpeg_path, [
'-loglevel', 'warning',
'-f', 'lavfi',
'-i', 'anullsrc',
'-i', '-',
'-vcodec', 'libx264',
'-r', fps,
'-g', fps * 2,
'-keyint_min', fps,
'-pix_fmt', 'yuv420p',
'-b:v', '2500k',
'-b:a', '1k',
'-acodec', 'aac',
'-f', 'flv', url + "/" + key,
], {
stdio: ["pipe"]
});
ffmpeg.stdout.on('data', (data) => {
console.log(`stdout: ${data.length}`);
});
ffmpeg.stderr.on('data', function(data) {
console.log(`stderr: ${data}`);
// streamStart({ url, key });
});
ffmpeg.on('close', (code) => {
console.log(`ffmpeg закончил свою работу: ${code}`);
streamStart({ url, key });
});
canvasUpdate();
writeStream({ ffmpeg });
}
function gameStep({ text, user }) {
if (!/^[wasd]+$/i.test(text)) return;
last_comment = `${user}: ${text}`;
text.toLowerCase().split('').map(function(dir) {
if (badDirections.indexOf(dir + dir_old) > -1) {
return;
}
dir_old = dir;
if (!snake_upgrade) snake.pop();
snake_upgrade = false;
snake.unshift(snake[0].join(",").split(",").map(x => x * 1));
if (dir === "w") snake[0][1]--;
if (dir === "s") snake[0][1]++;
if (dir === "a") snake[0][0]--;
if (dir === "d") snake[0][0]++;
if (snake[0][0] < 0) snake[0][0] = 63;
if (snake[0][0] > 63) snake[0][0] = 0;
if (snake[0][1] < 0) snake[0][1] = 70 / 2;
if (snake[0][1] > 70 / 2) snake[0][1] = 0;
if (snake.filter((p, i) => i && snake[0][0] === p[0] && snake[0][1] === p[1]).length) {
snake = [
[32, 18],
[32, 17],
[32, 16],
[32, 15]
];
}
if (snake[0][0] === apple[0] && snake[0][1] === apple[1]) {
snake_upgrade = true;
if (!score[user]) score[user] = 0;
score[user]++;
apple[0] = Math.floor(Math.random() * 63);
apple[1] = Math.floor(Math.random() * 35);
}
});
saveProgress();
}
function drawText(x, y, text, color, textBaseline, textAlign) {
ctx.textBaseline = textBaseline;
ctx.textAlign = textAlign;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
}
function canvasUpdate() {
if (backgroundHue++ > 360) backgroundHue = 0;
ctx.fillStyle = `hsl(${backgroundHue},60%,10%)`;
ctx.fillRect(0, 0, 1280, 720);
ctx.fillStyle = `hsl(${backgroundHue},65%,15%)`;
for (let x = 0; x < 70; x++) {
for (let y = 0; y < 70; y++) {
ctx.fillRect((x * 20) + 8, (y * 20) + 8, 4, 4);
}
}
const textColor = `hsl(${backgroundHue},60%,40%)`;
drawText(1260, 20, `Сейчас смотрят: ${views}\nДлина змейки: ${snake.length}`, textColor, 'top', 'right');
var top = "Топ:\n" + Object.keys(score)
.map(u => [u, score[u]])
.sort((b, a) => a[1] - b[1])
.splice(0, 10)
.map(([user_id, score]) => user_id + ": " + score)
.join('\n');
const time = (new Date().toTimeString().replace(/\(.+/, ''));
drawText(20, 20, top, textColor, 'top', 'left');
const lineSize = 18;
[
`hue: ${backgroundHue}`,
`Время: ${time}`,
`Последний ход: ${last_comment}`,
`Суперсообщение: ${last_super_comment}`
].reverse().forEach((text, index) => {
drawText(20, 700 - (index * lineSize), text, textColor, 'bottom', 'left');
});
[
`Максимум 5 ходов в одном сообщении`,
`В суперсообщениях максимум 50 символов`,
`Управление из комментариев клавишами WASD`
].reverse().forEach((text, index) => {
drawText(1260, 700 - (index * lineSize), text, textColor, 'bottom', 'right')
});
snake.map(function(point, index) {
const isFirst = !index;
const colorLight = isFirst ? 80 : 65;
ctx.fillStyle = `hsl(${backgroundHue},60%,${colorLight}%)`;
ctx.fillRect((point[0] * 20) + 1, (point[1] * 20) + 1, 18, 18);
});
ctx.fillStyle = `hsl(${backgroundHue},60%,90%)`;
ctx.fillRect(apple[0] * 20, apple[1] * 20, 20, 20);
const x = canvas.toBuffer((err, buffer) => {
if (!err) {
lastImage = buffer;
}
setTimeout(canvasUpdate, 500);
}, 'image/jpeg', {
quality: 100,
progressive: false
});
}
function writeStream({ ffmpeg }) {
if (lastImage) {
ffmpeg.stdio[0].write(lastImage);
}
setTimeout(writeStream, 1000 / fps, { ffmpeg });
}
function LongPoll_init(initData) {
streamer("video.getLongPollServer", {
video_id: initData.video_id,
owner_id: owner_id,
}).then((data) => {
console.log('LongPoll_init', data);
if (!data.url) {
throw data;
}
LongPoll_pull({
initData,
url: data.url,
});
}).catch((error) => {
console.error(error);
});
}
function LongPoll_pull({ initData, url }) {
request(url).then((body) => {
return JSON.parse(body);
}).then(({ ts, failed = false, events = [] }) => {
console.log('LongPoll_pull', { ts, failed });
if (ts) {
url = url.replace(/ts=\d+/, "ts=" + ts);
}
if (failed) {
LongPoll_init(initData);
return;
}
events.map(function(event) {
try {
return JSON.parse(event.replace(/<.+?>\d+$/, ""));
} catch (error) {
console.error('event error', event, error);
return {};
}
}).forEach((event) => {
LongPoll_onEvent(event);
});
setTimeout(LongPoll_pull, 100, { initData, url });
});
}
function LongPoll_onEvent(event) {
switch (event.type) {
case "video_special_comment_new":
case "video_comment_new":
console.log("Новый комментарий:", event.user.first_name, event.comment.from_id);
let text = event.comment.text || '';
let user = event.user.first_name + " " + event.user.last_name + ' @id' + event.user.id + '';
if (event.type !== 'video_special_comment_new') {
text = text.substr(0, 5);
} else {
last_super_comment = `${user}: ${text.replace(/\n/g, ' ').substr(0, 50)}`
}
gameStep({
text: text,
user: user
});
break;
case "video_view":
console.log("Сейчас смотрят:", event.count);
views = event.count;
break;
default:
console.log(event);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment