From 0e6683d60d70b3e51cbb6fcb4ed1c84e78a783b8 Mon Sep 17 00:00:00 2001 From: Izueh Date: Wed, 25 Dec 2024 03:51:03 -0800 Subject: [PATCH] Twitch Support (#84) (Thanks to @Izueh) * added twitch support * fixed config not accepting all encoding fixed streams not canceling properly added h26xPrese as env config * updated dependency fixed comment in Dockerfile --------- Co-authored-by: Isaac Ramos --- Dockerfile | 23 +++++++++++++++-------- package.json | 7 ++++--- src/config.ts | 39 ++++++++++++++++++++++++++++++++++++--- src/index.ts | 44 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2b7fbe3..2ad68a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,22 @@ -# Use the official nodejs 22 debian bookworm slim as the base image -FROM node:22.11-bookworm-slim +# Use ubuntu 22.04 as the base image +FROM ubuntu:22.04 # Set the working directory -WORKDIR /home/bots/StreamBot +WORKDIR /streambot -# Install important deps and clean cache -RUN apt-get update && \ - apt-get install -y -qq build-essential ffmpeg python3 && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* +# Install curl to fetch nodejs repository +RUN apt-get update && apt-get install -y curl + +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - +# Install important deps and clean cache +RUN apt-get update && apt-get install -y \ + build-essential \ + python3 \ + ffmpeg \ + nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* # Install pnpm RUN npm install pnpm -g diff --git a/package.json b/package.json index bae9b35..702326b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "author": "ysdragon", "license": "MIT", "dependencies": { - "@dank074/discord-video-stream": "4.1.4", + "@dank074/discord-video-stream": "4.1.5", "@distube/ytdl-core": "^4.15.1", "axios": "^1.7.9", "bcrypt": "^5.1.1", @@ -25,7 +25,8 @@ "multer": "^1.4.5-lts.1", "p-cancelable": "^4.0.1", "play-dl": "^1.9.7", - "tslib": "^2.8.1" + "tslib": "^2.8.1", + "twitch-m3u8": "^1.1.5" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -35,4 +36,4 @@ "@types/node": "^22.10.1", "typescript": "^5.7.2" } -} \ No newline at end of file +} diff --git a/src/config.ts b/src/config.ts index 6c1614e..dfe23c0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,10 +3,12 @@ import bcrypt from "bcrypt"; dotenv.config() +const VALID_VIDEO_CODECS = ['VP8', 'H264', 'H265', 'VP9', 'AV1']; + export default { // Selfbot options - token: process.env.TOKEN, - prefix: process.env.PREFIX, + token: process.env.TOKEN || '', + prefix: process.env.PREFIX || '', guildId: process.env.GUILD_ID ? process.env.GUILD_ID : '', cmdChannelId: process.env.COMMAND_CHANNEL_ID ? process.env.COMMAND_CHANNEL_ID : '', videoChannelId: process.env.VIDEO_CHANNEL_ID ? process.env.VIDEO_CHANNEL_ID : '', @@ -25,7 +27,8 @@ export default { bitrateKbps: process.env.STREAM_BITRATE_KBPS ? parseInt(process.env.STREAM_BITRATE_KBPS) : 1000, maxBitrateKbps: process.env.STREAM_MAX_BITRATE_KBPS ? parseInt(process.env.STREAM_MAX_BITRATE_KBPS) : 2500, hardwareAcceleratedDecoding: process.env.STREAM_HARDWARE_ACCELERATION ? parseBoolean(process.env.STREAM_HARDWARE_ACCELERATION) : false, - videoCodec: process.env.STREAM_VIDEO_CODEC === 'VP8' ? 'VP8' : 'H264', + h26xPreset: process.env.STREAM_H26X_PRESET ? parsePreset(process.env.STREAM_H26X_PRESET) : 'ultrafast', + videoCodec: process.env.STREAM_VIDEO_CODEC ? parseVideoCodec(process.env.STREAM_VIDEO_CODEC) : 'H264', // Videos server options server_enabled: process.env.SERVER_ENABLED ? parseBoolean(process.env.SERVER_ENABLED) : false, @@ -34,6 +37,36 @@ export default { server_port: parseInt(process.env.SERVER_PORT ? process.env.SERVER_PORT : '8080'), } +function parseVideoCodec(value: string): "VP8" | "H264" | "H265" { + if (typeof value === "string") { + value = value.trim().toUpperCase(); + } + if (VALID_VIDEO_CODECS.includes(value)) { + return value as "VP8" | "H264" | "H265"; + } + return "H264"; +} + +function parsePreset(value: string): "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow" { + if (typeof value === "string") { + value = value.trim().toLowerCase(); + } + switch (value) { + case "ultrafast": + case "superfast": + case "veryfast": + case "faster": + case "fast": + case "medium": + case "slow": + case "slower": + case "veryslow": + return value as "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow"; + default: + return "ultrafast"; + } +} + function parseBoolean(value: string | undefined): boolean { if (typeof value === "string") { value = value.trim().toLowerCase(); diff --git a/src/index.ts b/src/index.ts index ea4f156..a783508 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,20 @@ import { Client, TextChannel, CustomStatus, ActivityOptions, MessageAttachment } from "discord.js-selfbot-v13"; import { streamLivestreamVideo, MediaUdp, StreamOptions, Streamer, Utils } from "@dank074/discord-video-stream"; -import config from "./config.js" +import config from "./config.js"; import fs from 'fs'; import path from 'path'; import ytdl from '@distube/ytdl-core'; +import { getStream } from 'twitch-m3u8'; import yts from 'play-dl'; import ffmpeg from 'fluent-ffmpeg'; import { getVideoParams, ffmpegScreenshot } from "./utils/ffmpeg.js"; -import PCancelable from "p-cancelable"; +import PCancelable, {CancelError} from "p-cancelable"; + +interface TwitchStream { + quality: string; + resolution: string; + url: string; +} const streamer = new Streamer(new Client()); let command: PCancelable | undefined; @@ -32,7 +39,7 @@ const streamOpts: StreamOptions = { * Encoding preset for H264 or H265. The faster it is, the lower the quality * Available presets: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow */ - h26xPreset: 'ultrafast', + h26xPreset: config.h26xPreset, /** * Adds ffmpeg params to minimize latency and start outputting video as fast as possible. * Might create lag in video output in some rare cases @@ -243,6 +250,17 @@ streamer.client.on('messageCreate', async (message) => { } } break; + case link.includes('twitch.tv'): + { + const twitchId = link.split('/').pop() as string; + const twitchUrl = await getTwitchStreamUrl(twitchId); + if (twitchUrl) { + message.reply('**Playing...**'); + playVideo(twitchUrl, streamLinkUdpConn); + streamer.client.user?.setActivity(status_watch(`twitch.tv/${twitchId}`) as unknown as ActivityOptions); + } + } + break; default: { playVideo(link, streamLinkUdpConn); @@ -322,7 +340,7 @@ streamer.client.on('messageCreate', async (message) => { } command?.cancel() - + console.log("Stopped playing") message.reply('**Stopped playing.**'); } @@ -489,7 +507,7 @@ async function playVideo(video: string, udpConn: MediaUdp) { udpConn.mediaConnection.setVideoStatus(true); try { - const command = streamLivestreamVideo(video, udpConn); + command = streamLivestreamVideo(video, udpConn); const res = await command; console.log("Finished playing video " + res); @@ -505,7 +523,9 @@ async function playVideo(video: string, udpConn: MediaUdp) { }); } } catch (error) { - console.log("Error playing video: ", error); + if ( !(error instanceof CancelError) ) { + console.error("Error occurred while playing video:", error); + } } finally { udpConn.mediaConnection.setSpeaking(false); udpConn.mediaConnection.setVideoStatus(false); @@ -587,6 +607,18 @@ async function ytVideoCache(ytVideo: any): Promise { return null; } +async function getTwitchStreamUrl(url: string): Promise { + try { + const streams = await getStream(url); + const stream = streams.find((stream: TwitchStream) => stream.resolution === `${config.width}x${config.height}`) || streams[0]; + return stream.url; + // match best resolution with configured resolution + } catch (error) { + console.error("Error occurred while getting Twitch stream URL:", error); + return null; + } +} + async function getVideoUrl(videoUrl: string): Promise { try { const video = await ytdl.getInfo(videoUrl, { playerClients: ['WEB', 'ANDROID'] });