Skip to content

Commit

Permalink
Twitch Support (#84) (Thanks to @Izueh)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Izueh and eaIsaac authored Dec 25, 2024
1 parent 3536ea7 commit 0e6683d
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 20 deletions.
23 changes: 15 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -35,4 +36,4 @@
"@types/node": "^22.10.1",
"typescript": "^5.7.2"
}
}
}
39 changes: 36 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 : '',
Expand All @@ -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,
Expand All @@ -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();
Expand Down
44 changes: 38 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string> | undefined;
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -322,7 +340,7 @@ streamer.client.on('messageCreate', async (message) => {
}

command?.cancel()

console.log("Stopped playing")
message.reply('**Stopped playing.**');
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -587,6 +607,18 @@ async function ytVideoCache(ytVideo: any): Promise<string | null> {
return null;
}

async function getTwitchStreamUrl(url: string): Promise<string | null> {
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<string | null> {
try {
const video = await ytdl.getInfo(videoUrl, { playerClients: ['WEB', 'ANDROID'] });
Expand Down

0 comments on commit 0e6683d

Please sign in to comment.