Last active
March 25, 2021 13:29
-
-
Save FlyInk13/781f38c954fdec7ed7a8d99585507a8e to your computer and use it in GitHub Desktop.
Отрисовка змейки на node/canvas и стриминг ВКонтакте через ffmpeg. (Для запуска нужен токен VK Live https://vk.cc/6njXbx)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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