Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Discord-RE/Discord-video-stream
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.0.0
Choose a base ref
...
head repository: Discord-RE/Discord-video-stream
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.1.0
Choose a head ref
  • 4 commits
  • 23 files changed
  • 2 contributors

Commits on Oct 12, 2024

  1. update examples

    dank074 committed Oct 12, 2024
    Copy the full SHA
    bb658ad View commit details
  2. update readme

    dank074 committed Oct 12, 2024
    Copy the full SHA
    823338d View commit details

Commits on Oct 28, 2024

  1. Use ffmpeg demuxer instead of separate streams (#108)

    * Add ffmpeg demuxer
    
    * Return sample rate instead of frame rate in audio stream
    
    * I forgot package.json
    
    * Only allow opus
    
    With raw audio formats, we can't maintain the strict 1 Opus frame per write() guarantee, so instead force the user to provide Opus audio
    
    * No `any`
    
    * Adapt `VideoStream` and `AudioStream` to receive `AVPacket`
    
    * Rewrite `streamLivestreamVideo` to use the new demuxer
    
    Again, the situation with `streamOptions` isn't really what I want, but let's progress little by little for now
    
    * Add pts conversion function
    
    * Store pts in the media streams
    
    * Add rudimentary syncing mechanism
    
    * Use `setImmediate`
    
    * Remove unnecessary `setImmediate`
    
    * Export demuxer function
    
    * Update `libav.js`
    
    * Force matroska input
    
    Without an explicit input format, ffmpeg errors with "Invalid data when processing input"
    
    * Rewrite read loop
    
    * How did this slip through?????
    
    * Reduce output limit
    
    Lower values lead to lower latency, but might increase overhead of repeatedly calling the function. 16KiB of output data per call seems reasonable
    
    * Add ability to adjust sync tolerance
    
    * `matroska`, not `mkv`
    
    * Add `BaseMediaStream` class, attempts to fix infinite loops
    
    * Rework `libav.js` loading + use threads
    
    * Use `pipe` instead of `pipeline`
    
    * Setup stream syncing
    
    * `libopus` instead of `opus`
    
    * Use unified stream output mode & combine the loops
    
    * Add width and height information to demuxer output
    
    * Oops forgot to break
    
    * VP8 is simply `libvpx`
    
    * Add function to split and merge NAL units
    
    This will come into play later on
    
    * Synthesize parameter sets NAL units
    
    It turns out, the `mp4_h264toannexb` filter was helping us a great deal here, by inserting the parameter sets before each I frame. With mkv and other containers, they're only specified once in the file, so we need to synthesize it ourselves
    
    * Fix unhandled exception on stream end
    
    * Stop ffmpeg on demux failure
    
    * Do not spin wait
    
    While spin waiting will produce the most accurate syncing, it's also very CPU intensive (briefly peaking a single core of an i3-330M), and most of the time it's not really needed anyway. Using a timeout of 1ms is better.
    
    Ideally there will be a signaling mechanism between the 2 stream, instead of each stream polling eachother, but I'm keeping this simple.
    
    * More code dedupe + possible memory leak prevention
    
    * Unused import
    
    * Use libav.js in-built function for timestamp conversion
    
    Avoiding `BigInt` if possible is always good
    
    * Update all deps
    longnguyen2004 authored Oct 28, 2024
    Copy the full SHA
    7808635 View commit details
  2. new version

    dank074 committed Oct 28, 2024
    Copy the full SHA
    55cd997 View commit details
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Discord bot video
# Discord self-bot video
Fork: [Discord-video-experiment](https://github.com/mrjvs/Discord-video-experiment)

> [!CAUTION]
> Using any kind of automation programs on your account can result in your account getting permanently banned by Discord. Use at your own risk
This project implements the custom Discord UDP protocol for sending media. Since Discord is likely change their custom protocol, this library is subject to break at any point. An effort will be made to keep this library up to date with the latest Discord protocol, but it is not guranteed.

For better stability it is recommended to use WebRTC protocol instead since Discord is forced to adhere to spec, which means that the non-signaling code is guaranteed to work.
For better stability it is recommended to use WebRTC protocol instead since Discord is forced to adhere to spec, which means that the non-signaling portion of the code is guaranteed to work.

## Features
- Playing vp8 or h264 video in a voice channel (`go live`, or webcam video)
- Playing opus audio in a voice channel
- Playing video & audio in a voice channel (`Go Live`, or webcam video)

## Implementation
What I implemented and what I did not.
@@ -27,6 +29,10 @@ What I implemented and what I did not.
- [X] Regular Voice Connection
- [X] Go live

### Encryption
- [X] Transport Encryption
- [ ] https://github.com/dank074/Discord-video-stream/issues/102

#### Extras
- [X] Figure out rtp header extensions (discord specific) (discord seems to use one-byte RTP header extension https://www.rfc-editor.org/rfc/rfc8285.html#section-4.2)

@@ -57,7 +63,7 @@ npm install @dank074/discord-video-stream@latest
npm install discord.js-selfbot-v13@latest
```

Create a new client, and patch its events to listen for voice gateway events:
Create a new Streamer, and pass it a selfbot Client
```typescript
import { Client } from "discord.js-selfbot-v13";
import { Streamer } from '@dank074/discord-video-stream';
@@ -81,11 +87,17 @@ Start sending media over the udp connection:
udp.mediaConnection.setSpeaking(true);
udp.mediaConnection.setVideoStatus(true);
try {
const res = await streamLivestreamVideo("DIRECT VIDEO URL OR READABLE STREAM HERE", udp);
const cancellableCommand = await streamLivestreamVideo("DIRECT VIDEO URL OR READABLE STREAM HERE", udp);

const result = await cancellableCommand;
console.log("Finished playing video " + res);
} catch (e) {
console.log(e);
if (command.isCanceled) {
// Handle the cancelation here
console.log('Ffmpeg command was cancelled');
} else {
console.log(e);
}
} finally {
udp.mediaConnection.setSpeaking(false);
udp.mediaConnection.setVideoStatus(false);
@@ -135,6 +147,15 @@ rtcpSenderReportEnabled?: boolean;
* Encoding preset for H264 or H265. The faster it is, the lower the quality
*/
h26xPreset?: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow';
/**
* Adds ffmpeg params to minimize latency and start outputting video as fast as possible.
* Might create lag in video output in some rare cases
*/
minimizeLatency?: boolean;
/**
* ChaCha20-Poly1305 Encryption is faster than AES-256-GCM, except when using AES-NI
*/
forceChacha20Encryption?: boolean;
```
## Running example
`examples/basic/src/config.json`:
2 changes: 1 addition & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
"types": "dist/index.d.ts",
"type": "module",
"dependencies": {
"@dank074/discord-video-stream": "file:../..",
"@dank074/discord-video-stream": "4.0.0",
"@discordjs/opus": "^0.9.0",
"discord.js-selfbot-v13": "^3.1.4"
},
5 changes: 3 additions & 2 deletions examples/custom-stream-copy-codec/package.json
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"dependencies": {
"@dank074/discord-video-stream": "3.2.0",
"@dank074/discord-video-stream": "4.0.0",
"@discordjs/opus": "^0.9.0",
"discord.js-selfbot-v13": "^3.1.4",
"@dank074/fluent-ffmpeg-multistream-ts": "^1.0.2",
@@ -15,7 +16,7 @@
"devDependencies": {
"@types/node": "^18.14.1",
"@types/fluent-ffmpeg": "^2.1.21",
"typescript": "^4.9.5"
"typescript": "^5.6.2"
},
"scripts": {
"build": "tsc",
4 changes: 2 additions & 2 deletions examples/custom-stream-copy-codec/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MediaUdp, Streamer, getInputMetadata, inputHasAudio } from "@dank074/discord-video-stream";
import config from "./config.json";
import config from "./config.json" with {type: "json"};
import { Client, StageChannel } from "discord.js-selfbot-v13";
import { customFfmpegCommand, customStreamVideo } from "./customStream";
import { customFfmpegCommand, customStreamVideo } from "./customStream.js";

const streamer = new Streamer(new Client());

6 changes: 3 additions & 3 deletions examples/custom-stream-copy-codec/tsconfig.json
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */

/* Language and Environment */
"target": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
@@ -25,9 +25,9 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

/* Modules */
"module": "CommonJS", /* Specify what module code is generated. */
"module": "NodeNext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
9 changes: 5 additions & 4 deletions examples/puppeteer-stream/package.json
Original file line number Diff line number Diff line change
@@ -4,16 +4,17 @@
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type":"module",
"dependencies": {
"@dank074/discord-video-stream": "3.2.0",
"@dank074/discord-video-stream": "4.0.0",
"@discordjs/opus": "^0.9.0",
"discord.js-selfbot-v13": "^3.1.4",
"puppeteer": "^19.11.1",
"puppeteer-stream": "^2.1.4"
"puppeteer": "^23.5.3",
"puppeteer-stream": "^3.0.19"
},
"devDependencies": {
"@types/node": "^18.19.8",
"typescript": "^4.9.5"
"typescript": "^5.6.2"
},
"scripts": {
"build": "tsc",
24 changes: 17 additions & 7 deletions examples/puppeteer-stream/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { MediaUdp, Streamer, command, streamLivestreamVideo } from '@dank074/discord-video-stream';
import { MediaUdp, Streamer, streamLivestreamVideo, Utils } from '@dank074/discord-video-stream';
import { Client, StageChannel } from 'discord.js-selfbot-v13';
import { executablePath } from 'puppeteer';
import { launch, getStream } from 'puppeteer-stream';
import config from "./config.json";
import config from "./config.json" with {type: "json"};
import { Readable } from 'node:stream';
import PCancelable from "p-cancelable";

const streamer = new Streamer(new Client());
let command: PCancelable<string>;

// ready event
streamer.client.on("ready", () => {
@@ -47,7 +49,7 @@ streamer.client.on("messageCreate", async (msg) => {
bitrateKbps: config.streamOpts.bitrateKbps,
maxBitrateKbps: config.streamOpts.maxBitrateKbps,
hardwareAcceleratedDecoding: config.streamOpts.hardware_acceleration,
videoCodec: config.streamOpts.videoCodec === 'H264' ? 'H264' : 'VP8'
videoCodec: "VP8" // puppeteer only supports this video codec
});

await streamPuppeteer(url, streamUdpConn);
@@ -56,12 +58,15 @@ streamer.client.on("messageCreate", async (msg) => {

return;
} else if (msg.content.startsWith("$disconnect")) {
command?.kill("SIGINT");
command?.cancel();

streamer.leaveVoice();
}
})

// login
streamer.client.login(config.token);

async function streamPuppeteer(url: string, udpConn: MediaUdp) {
const streamOpts = udpConn.mediaConnection.streamOptions;

@@ -83,14 +88,19 @@ async function streamPuppeteer(url: string, udpConn: MediaUdp) {
udpConn.mediaConnection.setVideoStatus(true);
try {
// is there a way to distinguish audio from video chunks so we dont have to use ffmpeg ???
const res = await streamLivestreamVideo((stream as Readable), udpConn);
command = streamLivestreamVideo((stream as Readable), udpConn);

const res = await command;
console.log("Finished playing video " + res);
} catch (e) {
console.log(e);
if (command.isCanceled) {
// Handle the cancelation here
console.log('Operation was canceled');
} else {
console.log(e);
}
} finally {
udpConn.mediaConnection.setSpeaking(false);
udpConn.mediaConnection.setVideoStatus(false);
}
command?.kill("SIGINT");
}
6 changes: 3 additions & 3 deletions examples/puppeteer-stream/tsconfig.json
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */

/* Language and Environment */
"target": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
@@ -25,9 +25,9 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

/* Modules */
"module": "CommonJS", /* Specify what module code is generated. */
"module": "NodeNext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dank074/discord-video-stream",
"version": "4.0.0",
"version": "4.1.0",
"description": "Experiment for making video streaming work for discord selfbots",
"exports": "./dist/index.js",
"types": "dist/index.d.ts",
@@ -14,19 +14,21 @@
},
"dependencies": {
"@dank074/fluent-ffmpeg-multistream-ts": "^1.0.2",
"fluent-ffmpeg": "^2.1.2",
"libsodium-wrappers": "^0.7.13",
"@libav.js/variant-webcodecs": "^6.4.7",
"fluent-ffmpeg": "^2.1.3",
"libsodium-wrappers": "^0.7.15",
"opusscript": "^0.1.1",
"p-cancelable": "^4.0.1",
"prism-media": "^1.3.5",
"ws": "^8.16.0"
"uid": "^2.0.2",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/fluent-ffmpeg": "^2.1.24",
"@types/fluent-ffmpeg": "^2.1.27",
"@types/libsodium-wrappers": "^0.7.14",
"@types/node": "^20.12.7",
"@types/ws": "^8.5.10",
"typescript": "^5.4.5"
"@types/node": "^20.17.1",
"@types/ws": "^8.5.12",
"typescript": "^5.6.3"
},
"peerDependencies": {
"discord.js-selfbot-v13": "3.x"
4 changes: 2 additions & 2 deletions src/client/packet/AudioPacketizer.ts
Original file line number Diff line number Diff line change
@@ -9,14 +9,14 @@ export class AudioPacketizer extends BaseMediaPacketizer {
this.srInterval = 5 * 48000 / frame_size; // ~5 seconds
}

public override sendFrame(frame:any): void {
public override sendFrame(frame: Buffer): void {
super.sendFrame(frame);
const packet = this.createPacket(frame);
this.mediaUdp.sendPacket(packet);
this.onFrameSent(packet.length);
}

public createPacket(chunk: any): Buffer {
public createPacket(chunk: Buffer): Buffer {
const header = this.makeRtpHeader();

const nonceBuffer = this.mediaUdp.getNewNonceBuffer();
2 changes: 1 addition & 1 deletion src/client/packet/BaseMediaPacketizer.ts
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ export class BaseMediaPacketizer {
this._srInterval = interval;
}

public sendFrame(frame:any): void {
public sendFrame(frame: Buffer): void {
// override this
this._lastPacketTime = Date.now();
}
13 changes: 2 additions & 11 deletions src/client/packet/VideoPacketizerAnnexB.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import {
type AnnexBHelpers
} from "../processing/AnnexBHelper.js";
import { extensions } from "../../utils.js";
import { splitNalu } from "../processing/AnnexBHelper.js";

/**
* Annex B format
@@ -72,18 +73,8 @@ class VideoPacketizerAnnexB extends BaseMediaPacketizer {
*/
public override sendFrame(frame: Buffer): void {
super.sendFrame(frame);
let accessUnit = frame;

const nalus: Buffer[] = [];

let offset = 0;
while (offset < accessUnit.length) {
const naluSize = accessUnit.readUInt32BE(offset);
offset += 4;
const nalu = accessUnit.subarray(offset, offset + naluSize);
nalus.push(nalu);
offset += nalu.length;
}
const nalus = splitNalu(frame);

let packetsSent = 0;
let bytesSent = 0;
29 changes: 28 additions & 1 deletion src/client/processing/AnnexBHelper.ts
Original file line number Diff line number Diff line change
@@ -112,4 +112,31 @@ export const H265Helpers: AnnexBHelpers = {
isAUD(unitType) {
return unitType === H265NalUnitTypes.AUD_NUT;
}
}
}

// Get individual NAL units from an AVPacket frame
export function splitNalu(frame: Buffer) {
const nalus = [];
let offset = 0;
while (offset < frame.length) {
const naluSize = frame.readUInt32BE(offset);
offset += 4;
const nalu = frame.subarray(offset, offset + naluSize);
nalus.push(nalu);
offset += nalu.length;
}
return nalus;
}

// Merge NAL units into an AVPacket frame
export function mergeNalu(nalus: Buffer[])
{
const chunks = [];
for (const nalu of nalus)
{
const size = Buffer.allocUnsafe(4);
size.writeUInt32BE(nalu.length);
chunks.push(size, nalu);
}
return Buffer.concat(chunks);
}
5 changes: 2 additions & 3 deletions src/client/voice/MediaUdp.ts
Original file line number Diff line number Diff line change
@@ -85,14 +85,13 @@ export class MediaUdp {
this._encryptionMode = mode;
}

public sendAudioFrame(frame: any): void{
public sendAudioFrame(frame: Buffer): void{
if(!this.ready) return;
this.audioPacketizer.sendFrame(frame);
}

public sendVideoFrame(frame: any): void {
public sendVideoFrame(frame: Buffer): void {
if(!this.ready) return;

this.videoPacketizer.sendFrame(frame);
}

Loading