piixishstream 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,33 +82,47 @@ export class Streamer {
82
82
  reject("cannot start stream without first joining voice channel");
83
83
  return;
84
84
  }
85
- this.signalStream();
86
- const { guildId: clientGuildId, channelId: clientChannelId, session_id, } = this.voiceConnection;
85
+ if (this.voiceConnection.streamConnection?.webRtcConn?.ready) {
86
+ resolve(this.voiceConnection.streamConnection.webRtcConn);
87
+ return;
88
+ }
89
+ const { guildId: clientGuildId, channelId: clientChannelId, session_id } = this.voiceConnection;
87
90
  const { id: clientUserId } = this.client.user;
88
- if (!session_id)
89
- throw new Error("Session doesn't exist yet");
90
- const streamConn = new StreamConnection(this, clientGuildId, clientUserId, clientChannelId, (conn) => {
91
- resolve(conn);
92
- });
93
- this.voiceConnection.streamConnection = streamConn;
94
- this._gatewayEmitter.on("STREAM_CREATE", (d) => {
91
+ if (!session_id) throw new Error("Session doesn't exist yet");
92
+
93
+ let done = false;
94
+ const onStreamCreate = (d) => {
95
95
  const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
96
- if (clientGuildId !== guildId ||
97
- clientChannelId !== channelId ||
98
- clientUserId !== userId)
99
- return;
96
+ if (clientGuildId !== guildId || clientChannelId !== channelId || clientUserId !== userId) return;
100
97
  streamConn.serverId = d.rtc_server_id;
101
98
  streamConn.streamKey = d.stream_key;
102
99
  streamConn.setSession(session_id);
103
- });
104
- this._gatewayEmitter.on("STREAM_SERVER_UPDATE", (d) => {
100
+ };
101
+ const onStreamServerUpdate = (d) => {
105
102
  const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
106
- if (clientGuildId !== guildId ||
107
- clientChannelId !== channelId ||
108
- clientUserId !== userId)
109
- return;
103
+ if (clientGuildId !== guildId || clientChannelId !== channelId || clientUserId !== userId) return;
110
104
  streamConn.setTokens(d.endpoint, d.token);
105
+ };
106
+ const cleanup = () => {
107
+ if (done) return;
108
+ done = true;
109
+ clearTimeout(timer);
110
+ this._gatewayEmitter.off("STREAM_CREATE", onStreamCreate);
111
+ this._gatewayEmitter.off("STREAM_SERVER_UPDATE", onStreamServerUpdate);
112
+ };
113
+ const timer = setTimeout(() => {
114
+ cleanup();
115
+ reject(new Error("Timeout en attente de la passerelle Discord Go-Live"));
116
+ }, 10000);
117
+
118
+ this.signalStream();
119
+ const streamConn = new StreamConnection(this, clientGuildId, clientUserId, clientChannelId, (conn) => {
120
+ cleanup();
121
+ resolve(conn);
111
122
  });
123
+ this.voiceConnection.streamConnection = streamConn;
124
+ this._gatewayEmitter.on("STREAM_CREATE", onStreamCreate);
125
+ this._gatewayEmitter.on("STREAM_SERVER_UPDATE", onStreamServerUpdate);
112
126
  });
113
127
  }
114
128
  async setStreamPreview(image) {
@@ -60,6 +60,25 @@ export class WebRtcConnWrapper {
60
60
  get webRtcConn() {
61
61
  return this._webRtcConn;
62
62
  }
63
+ waitUntilReady(timeoutMs = 5000) {
64
+ if (this.ready) return Promise.resolve();
65
+ return new Promise((resolve) => {
66
+ let done = false;
67
+ const finish = () => {
68
+ if (!done) {
69
+ done = true;
70
+ clearTimeout(timer);
71
+ resolve();
72
+ }
73
+ };
74
+ const timer = setTimeout(finish, timeoutMs);
75
+ this._webRtcConn?.onStateChange((state) => {
76
+ if (state === "connected") {
77
+ finish();
78
+ }
79
+ });
80
+ });
81
+ }
63
82
  get ready() {
64
83
  return this._webRtcConn?.state() === "connected";
65
84
  }
@@ -33,6 +33,7 @@ export function prepareStream(input, options = {}, cancelSignal) {
33
33
  },
34
34
  customInputOptions: [],
35
35
  customFfmpegFlags: [],
36
+ audioInput: undefined,
36
37
  };
37
38
  function mergeOptions(opts) {
38
39
  return {
@@ -67,6 +68,7 @@ export function prepareStream(input, options = {}, cancelSignal) {
67
68
  },
68
69
  customInputOptions: opts.customInputOptions ?? defaultOptions.customInputOptions,
69
70
  customFfmpegFlags: opts.customFfmpegFlags ?? defaultOptions.customFfmpegFlags,
71
+ audioInput: opts.audioInput ?? defaultOptions.audioInput,
70
72
  };
71
73
  }
72
74
  const mergedOptions = mergeOptions(options);
@@ -80,8 +82,11 @@ export function prepareStream(input, options = {}, cancelSignal) {
80
82
  }
81
83
  const output = new PassThrough();
82
84
  // command creation
83
- const command = ffmpeg(input);
84
- // input options
85
+ const command = ffmpeg(input);
86
+ if (mergedOptions.audioInput) {
87
+ command.input(mergedOptions.audioInput).inputOptions(["-stream_loop", "-1"]);
88
+ }
89
+ // input options
85
90
  if (mergedOptions.customInputOptions &&
86
91
  mergedOptions.customInputOptions.length > 0) {
87
92
  command.inputOptions(mergedOptions.customInputOptions);
@@ -144,10 +149,14 @@ export function prepareStream(input, options = {}, cancelSignal) {
144
149
  .outputOptions(encoderSettings.globalOptions ?? []);
145
150
  }
146
151
  // audio setup
147
- const { includeAudio, bitrateAudio } = mergedOptions;
148
- if (includeAudio)
152
+ const { includeAudio, bitrateAudio, audioInput } = mergedOptions;
153
+ if (includeAudio) {
154
+ if (audioInput) {
155
+ command.addOutputOption("-map 1:a");
156
+ } else {
157
+ command.addOutputOption("-map 0:a:0?");
158
+ }
149
159
  command
150
- .addOutputOption("-map 0:a:0?")
151
160
  .audioChannels(2)
152
161
  /*
153
162
  * I don't have much surround sound material to test this with,
@@ -274,6 +283,7 @@ export async function playStream(input, streamer, options = {}, cancelSignal) {
274
283
  ? opts.readrateInitialBurst
275
284
  : defaultOptions.readrateInitialBurst,
276
285
  streamPreview: opts.streamPreview ?? defaultOptions.streamPreview,
286
+ keepConnection: opts.keepConnection ?? false,
277
287
  };
278
288
  }
279
289
  const mergedOptions = mergeOptions(options);
@@ -297,12 +307,12 @@ export async function playStream(input, streamer, options = {}, cancelSignal) {
297
307
  let stopStream;
298
308
  if (mergedOptions.type === "go-live") {
299
309
  conn = await streamer.createStream();
300
- stopStream = () => streamer.stopStream();
310
+ stopStream = () => { if (!mergedOptions.keepConnection) streamer.stopStream(); };
301
311
  }
302
312
  else {
303
313
  conn = streamer.voiceConnection.webRtcConn;
304
314
  streamer.signalVideo(true);
305
- stopStream = () => streamer.signalVideo(false);
315
+ stopStream = () => { if (!mergedOptions.keepConnection) streamer.signalVideo(false); };
306
316
  }
307
317
  conn.setPacketizer(videoCodecMap[video.codec]);
308
318
  conn.mediaConnection.setSpeaking(true);
@@ -383,8 +393,10 @@ export async function playStream(input, streamer, options = {}, cancelSignal) {
383
393
  return new Promise((resolve, reject) => {
384
394
  cleanupFuncs.push(() => {
385
395
  stopStream();
386
- conn.mediaConnection.setSpeaking(false);
387
- conn.mediaConnection.setVideoAttributes(false);
396
+ if (!mergedOptions.keepConnection) {
397
+ conn.mediaConnection.setSpeaking(false);
398
+ conn.mediaConnection.setVideoAttributes(false);
399
+ }
388
400
  });
389
401
  let cleanedUp = false;
390
402
  const cleanup = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "piixishstream",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Experiment for making video streaming work for discord selfbots",
5
5
  "main": "./dist/index.js",
6
6
  "exports": "./dist/index.js",
@@ -81,3 +81,5 @@
81
81
  ]
82
82
  }
83
83
  }
84
+
85
+
@@ -104,63 +104,58 @@ export class Streamer {
104
104
  });
105
105
  }
106
106
 
107
- public createStream(): Promise<WebRtcConnWrapper> {
108
- return new Promise<WebRtcConnWrapper>((resolve, reject) => {
109
- if (!this.client.user) {
110
- reject("Client not logged in");
111
- return;
112
- }
113
- if (!this.voiceConnection) {
114
- reject("cannot start stream without first joining voice channel");
115
- return;
116
- }
117
-
118
- this.signalStream();
119
- const {
120
- guildId: clientGuildId,
121
- channelId: clientChannelId,
122
- session_id,
123
- } = this.voiceConnection;
124
- const { id: clientUserId } = this.client.user;
125
-
126
- if (!session_id) throw new Error("Session doesn't exist yet");
127
- const streamConn = new StreamConnection(
128
- this,
129
- clientGuildId,
130
- clientUserId,
131
- clientChannelId,
132
- (conn) => {
133
- resolve(conn);
134
- },
135
- );
136
- this.voiceConnection.streamConnection = streamConn;
137
- this._gatewayEmitter.on("STREAM_CREATE", (d) => {
138
- const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
139
-
140
- if (
141
- clientGuildId !== guildId ||
142
- clientChannelId !== channelId ||
143
- clientUserId !== userId
144
- )
145
- return;
146
-
147
- streamConn.serverId = d.rtc_server_id;
148
- streamConn.streamKey = d.stream_key;
149
- streamConn.setSession(session_id);
150
- });
151
- this._gatewayEmitter.on("STREAM_SERVER_UPDATE", (d) => {
152
- const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
153
-
154
- if (
155
- clientGuildId !== guildId ||
156
- clientChannelId !== channelId ||
157
- clientUserId !== userId
158
- )
159
- return;
160
-
161
- streamConn.setTokens(d.endpoint, d.token);
162
- });
163
- });
107
+ public createStream(): Promise<WebRtcConnWrapper> {
108
+ return new Promise<WebRtcConnWrapper>((resolve, reject) => {
109
+ if (!this.client.user) {
110
+ reject("Client not logged in");
111
+ return;
112
+ }
113
+ if (!this.voiceConnection) {
114
+ reject("cannot start stream without first joining voice channel");
115
+ return;
116
+ }
117
+ if (this.voiceConnection.streamConnection?.webRtcConn?.ready) {
118
+ resolve(this.voiceConnection.streamConnection.webRtcConn);
119
+ return;
120
+ }
121
+ const { guildId: clientGuildId, channelId: clientChannelId, session_id } = this.voiceConnection;
122
+ const { id: clientUserId } = this.client.user;
123
+ if (!session_id) throw new Error("Session doesn't exist yet");
124
+
125
+ let done = false;
126
+ const onStreamCreate = (d: any) => {
127
+ const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
128
+ if (clientGuildId !== guildId || clientChannelId !== channelId || clientUserId !== userId) return;
129
+ streamConn.serverId = d.rtc_server_id;
130
+ streamConn.streamKey = d.stream_key;
131
+ streamConn.setSession(session_id);
132
+ };
133
+ const onStreamServerUpdate = (d: any) => {
134
+ const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
135
+ if (clientGuildId !== guildId || clientChannelId !== channelId || clientUserId !== userId) return;
136
+ streamConn.setTokens(d.endpoint, d.token);
137
+ };
138
+ const cleanup = () => {
139
+ if (done) return;
140
+ done = true;
141
+ clearTimeout(timer);
142
+ this._gatewayEmitter.off("STREAM_CREATE", onStreamCreate);
143
+ this._gatewayEmitter.off("STREAM_SERVER_UPDATE", onStreamServerUpdate);
144
+ };
145
+ const timer = setTimeout(() => {
146
+ cleanup();
147
+ reject(new Error("Timeout en attente de la passerelle Discord Go-Live"));
148
+ }, 10000);
149
+
150
+ this.signalStream();
151
+ const streamConn = new StreamConnection(this, clientGuildId, clientUserId, clientChannelId, (conn) => {
152
+ cleanup();
153
+ resolve(conn);
154
+ });
155
+ this.voiceConnection.streamConnection = streamConn;
156
+ this._gatewayEmitter.on("STREAM_CREATE", onStreamCreate);
157
+ this._gatewayEmitter.on("STREAM_SERVER_UPDATE", onStreamServerUpdate);
158
+ });
164
159
  }
165
160
 
166
161
  public async setStreamPreview(image: Buffer): Promise<void> {
@@ -93,7 +93,26 @@ export class WebRtcConnWrapper {
93
93
  return this._webRtcConn;
94
94
  }
95
95
 
96
- public get ready() {
96
+ public waitUntilReady(timeoutMs = 5000) {
97
+ if (this.ready) return Promise.resolve();
98
+ return new Promise((resolve) => {
99
+ let done = false;
100
+ const finish = () => {
101
+ if (!done) {
102
+ done = true;
103
+ clearTimeout(timer);
104
+ resolve();
105
+ }
106
+ };
107
+ const timer = setTimeout(finish, timeoutMs);
108
+ this._webRtcConn?.onStateChange((state) => {
109
+ if (state === "connected") {
110
+ finish();
111
+ }
112
+ });
113
+ });
114
+ }
115
+ get ready() {
97
116
  return this._webRtcConn?.state() === "connected";
98
117
  }
99
118
 
@@ -137,7 +137,8 @@ export function prepareStream(
137
137
  Connection: "keep-alive",
138
138
  },
139
139
  customInputOptions: [],
140
- customFfmpegFlags: [],
140
+ customFfmpegFlags: [],
141
+ audioInput: undefined,
141
142
  } satisfies PrepareStreamOptions;
142
143
 
143
144
  function mergeOptions(opts: Partial<PrepareStreamOptions>) {
@@ -211,7 +212,12 @@ export function prepareStream(
211
212
 
212
213
  // command creation
213
214
  const command = ffmpeg(input);
214
-
215
+ if (mergedOptions.audioInput) {
216
+ command.input(mergedOptions.audioInput).inputOptions(["-stream_loop", "-1"]);
217
+ }
218
+ if (mergedOptions.audioInput) {
219
+ command.input(mergedOptions.audioInput).inputOptions(["-stream_loop", "-1"]);
220
+ }
215
221
  // input options
216
222
  if (
217
223
  mergedOptions.customInputOptions &&
@@ -298,10 +304,10 @@ export function prepareStream(
298
304
  }
299
305
 
300
306
  // audio setup
301
- const { includeAudio, bitrateAudio } = mergedOptions;
307
+ const { includeAudio, bitrateAudio, audioInput } = mergedOptions;
302
308
  if (includeAudio)
303
309
  command
304
- .addOutputOption("-map 0:a:0?")
310
+ .addOutputOption(mergedOptions.audioInput ? "-map 1:a" : "-map 0:a:0?")
305
311
  .audioChannels(2)
306
312
  /*
307
313
  * I don't have much surround sound material to test this with,
@@ -492,7 +498,8 @@ export async function playStream(
492
498
  ? opts.readrateInitialBurst
493
499
  : defaultOptions.readrateInitialBurst,
494
500
 
495
- streamPreview: opts.streamPreview ?? defaultOptions.streamPreview,
501
+ streamPreview: opts.streamPreview ?? defaultOptions.streamPreview,
502
+ keepConnection: opts.keepConnection ?? false,
496
503
  } satisfies PlayStreamOptions;
497
504
  }
498
505
 
@@ -520,11 +527,11 @@ export async function playStream(
520
527
  let stopStream: () => unknown;
521
528
  if (mergedOptions.type === "go-live") {
522
529
  conn = await streamer.createStream();
523
- stopStream = () => streamer.stopStream();
530
+ stopStream = () => { if (!mergedOptions.keepConnection) streamer.stopStream(); };
524
531
  } else {
525
532
  conn = streamer.voiceConnection.webRtcConn;
526
533
  streamer.signalVideo(true);
527
- stopStream = () => streamer.signalVideo(false);
534
+ stopStream = () => { if (!mergedOptions.keepConnection) streamer.signalVideo(false); };
528
535
  }
529
536
  conn.setPacketizer(videoCodecMap[video.codec]);
530
537
  conn.mediaConnection.setSpeaking(true);
@@ -616,8 +623,10 @@ export async function playStream(
616
623
  return new Promise<void>((resolve, reject) => {
617
624
  cleanupFuncs.push(() => {
618
625
  stopStream();
619
- conn.mediaConnection.setSpeaking(false);
620
- conn.mediaConnection.setVideoAttributes(false);
626
+ if (!mergedOptions.keepConnection) {
627
+ conn.mediaConnection.setSpeaking(false);
628
+ conn.mediaConnection.setVideoAttributes(false);
629
+ }
621
630
  });
622
631
  let cleanedUp = false;
623
632
  const cleanup = () => {