diff --git a/src/media/mediaController.ts b/src/media/mediaController.ts index c052b95..b25e4cf 100644 --- a/src/media/mediaController.ts +++ b/src/media/mediaController.ts @@ -17,7 +17,7 @@ import { createMusicPlayer } from "./musicPlayer"; export interface MediaControllerDependencies { isVoiceConnected?: () => boolean; isBrowserStreaming?: () => boolean; - resolveMediaSource?: (source: string) => Promise; + resolveMediaSource?: (source: string, mode?: MediaMode) => Promise; musicPlayer?: MusicPlayer; screenController?: ScreenShareController; onStateChange?: (state: MediaState) => void; @@ -73,12 +73,17 @@ export class MediaController { options: QueueMediaOptions = {}, ): Promise { const mode = options.mode ?? "music"; + + const resolved = await ( + this.dependencies.resolveMediaSource ?? resolveMediaSource + )(source, mode); + if (mode === "screen") { // Stop current music if any this.playbackToken++; this.playback?.stop(); this.playback = null; - return this.startScreen(source); + return this.startScreen(resolved.source); } // mode === "music" @@ -95,9 +100,6 @@ export class MediaController { } this.assertCanStartMusic(); - const resolved = await ( - this.dependencies.resolveMediaSource ?? resolveMediaSource - )(source); this.queueStore.add(resolved, mode, options.requestedBy); this.startNextIfIdle(); return this.emitState(); diff --git a/src/media/mediaResolver.ts b/src/media/mediaResolver.ts index eec68ff..4a4d525 100644 --- a/src/media/mediaResolver.ts +++ b/src/media/mediaResolver.ts @@ -1,7 +1,7 @@ import { existsSync, statSync } from "node:fs"; import path from "node:path"; import { AppError } from "../errors"; -import type { ResolvedMediaSource } from "./mediaTypes"; +import type { ResolvedMediaSource, MediaMode } from "./mediaTypes"; import { createPlayDlResolver } from "./playDlResolver"; import { createYtDlp, type YtDlpClient } from "./ytdlp"; @@ -18,7 +18,10 @@ export function createMediaResolver( const ytdlp = dependencies.ytdlp ?? createYtDlp(); const playDlResolver = dependencies.playDlResolver ?? createPlayDlResolver(); - return async function resolve(input: string): Promise { + return async function resolve( + input: string, + mode: MediaMode = "music" + ): Promise { const source = input.trim(); if (!source) { throw new AppError( @@ -31,13 +34,17 @@ export function createMediaResolver( const url = parseUrl(source); if (url && isYouTubeUrl(url)) { const metadata = await ytdlp.getMetadata(source); - const directUrl = await ytdlp.getDirectAudioUrl(source); + const directUrl = mode === "screen" + ? await ytdlp.getDirectVideoUrl(source) + : await ytdlp.getDirectAudioUrl(source); return { source: directUrl, title: metadata.title, kind: "youtube" }; } if (url && isSpotifyTrackUrl(url)) { const result = await playDlResolver.resolveSpotifyTrack(source); - const directUrl = await ytdlp.getDirectAudioUrl(result.url); + const directUrl = mode === "screen" + ? await ytdlp.getDirectVideoUrl(result.url) + : await ytdlp.getDirectAudioUrl(result.url); return { source: directUrl, title: result.title, kind: "spotify" }; } @@ -55,7 +62,9 @@ export function createMediaResolver( if (!url && !looksLikeUrl(source)) { const result = await playDlResolver.searchYouTube(source); - const directUrl = await ytdlp.getDirectAudioUrl(result.url); + const directUrl = mode === "screen" + ? await ytdlp.getDirectVideoUrl(result.url) + : await ytdlp.getDirectAudioUrl(result.url); return { source: directUrl, title: result.title, kind: "search" }; } diff --git a/src/media/ytdlp.ts b/src/media/ytdlp.ts index cf5c360..0330fc5 100644 --- a/src/media/ytdlp.ts +++ b/src/media/ytdlp.ts @@ -56,7 +56,7 @@ export function createYtDlp(dependencies: YtDlpDependencies = {}): YtDlpClient { url, "--get-url", "--format", - "bestvideo[protocol^=http]+bestaudio[protocol^=http]/best[protocol^=http]/best", + "best[protocol^=http]/best", "--no-playlist", "--no-warnings", "--quiet", diff --git a/src/streaming/index.ts b/src/streaming/index.ts index 348fbd0..5ef4a90 100644 --- a/src/streaming/index.ts +++ b/src/streaming/index.ts @@ -75,6 +75,7 @@ export class Streamer { const bitrateStr = String(options.bitrate ?? 2500).replace(/k$/i, ""); const bitrateVideo = parseInt(bitrateStr, 10) || 2500; + console.log("[Streamer] Starting screen share for source:", typeof targetSource === "string" ? targetSource.slice(0, 50) + "..." : "ReadableStream"); const { command, output } = dankPrepareStream(targetSource, { encoder: Encoders.software({ x264: { preset: (options.presetH26x as any) ?? "superfast" }, @@ -86,6 +87,8 @@ export class Streamer { bitrateVideo: bitrateVideo, frameRate: fps, includeAudio: options.includeAudio !== false, + minimizeLatency: false, + customInputOptions: ["-fflags nobuffer"], customHeaders: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3", @@ -95,6 +98,12 @@ export class Streamer { currentCommand = command; + const webOutput = new PassThrough(); + const discordOutput = new PassThrough(); + + output.pipe(webOutput); + output.pipe(discordOutput); + const globalAny: any = globalThis; const onData = (chunk: Buffer) => { try { @@ -103,21 +112,27 @@ export class Streamer { // ignore } }; - output.on("data", onData); + webOutput.on("data", onData); command.on("error", (err: Error) => { - console.error("Transcoder error:", err); + console.error("[Streamer] Transcoder error:", err); + }); + command.on("stderr", (stderrLine: string) => { + console.error("[Streamer] FFMPEG:", stderrLine); + }); + command.on("end", () => { + console.log("[Streamer] FFMPEG process ended naturally."); }); try { - await dankPlayStream(output, this.dankStreamer, { - type: "go-live", - width: 1280, - height: 720, - frameRate: fps, - }); + console.log("[Streamer] Calling dankPlayStream..."); + await dankPlayStream(discordOutput, this.dankStreamer, undefined); + console.log("[Streamer] dankPlayStream completed successfully."); + } catch (err) { + console.error("[Streamer] dankPlayStream error:", err); } finally { - output.off("data", onData); + console.log("[Streamer] Cleaning up stream resources."); + webOutput.off("data", onData); stop(); } }, diff --git a/test_dank.ts b/test_dank.ts new file mode 100644 index 0000000..fe95685 --- /dev/null +++ b/test_dank.ts @@ -0,0 +1,33 @@ +import { prepareStream, Encoders } from "@dank074/discord-video-stream"; +import fs from "fs"; + +async function run() { + console.log("Starting prepareStream..."); + const { command, output } = prepareStream("https://rr3---sn-2uuxa3vh-unte.googlevideo.com/videoplayback?expire=1779046518&ei=FsQJatGDGNqp9fwP4qz4SA&ip=180.252.24.35&id=o-APFvGry6yPgoap-1RT0pu59DxD-pcXC4oXtMQuCMtjOy&itag=18&source=youtube&requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&cps=618&met=1779024918%2C&mh=VD&mm=31%2C29&mn=sn-2uuxa3vh-unte%2Csn-oguelnze&ms=au%2Crdu&mv=m&mvi=3&pcm2cms=yes&pl=20&rms=au%2Cau&initcwndbps=763750&bui=AbKmrwofOLw_tOID4kBHnWgaXP2wnDlEYmbyHyrnZk1n7vjMaQIuY046T9MhH0PuL9JGJwj6YlwCr2Uu&spc=96Xrv8WI7iTS7MOF7Dvg-8a3RT-sMI9ux49zUa4Pg6GHkzXExSS0&vprv=1&svpuc=1&mime=video%2Fmp4&rqh=1&cnr=14&ratebypass=yes&dur=19.063&lmt=1772437158054287&mt=1779024581&fvip=4&fexp=51565116%2C51565681&c=ANDROID_VR&txp=4530534&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Crqh%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AHEqNM4wRgIhAJe1vu37ssUQQm3scVgXY7NYDx_frKW1AZ4gHRdcqsUlAiEAkKt6jxaCNvaEh6jag1OWheo5qQeu3ObfCCoQIZ9xnCA%3D&lsparams=cps%2Cmet%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpcm2cms%2Cpl%2Crms%2Cinitcwndbps&lsig=APaTxxMwRQIhAMkeJ6WrDFU7fTfSb6s_WbdDpn4J-4NqkfzKV3B_y1cgAiBJ7aExkhh-0hvIWwNorjDwoOkTIKIfmzx6o6Z3mxlazA%3D%3D", { + encoder: Encoders.software(), + width: 1280, + height: 720, + includeAudio: true, + minimizeLatency: false // Add this + }); + + const fileStream = fs.createWriteStream("/mnt/code/bete/test_out.nut"); + output.pipe(fileStream); + + command.on("error", (err, stdout, stderr) => { + console.error("FFMPEG ERROR:", err.message); + }); + command.on("stderr", (stderrLine) => { + console.log("FFMPEG LOG:", stderrLine); + }); + command.on("end", () => { + console.log("FFMPEG FINISHED"); + process.exit(0); + }); + + setTimeout(() => { + try { command.kill("SIGKILL"); } catch(e) {} + process.exit(0); + }, 10000); +} +run(); diff --git a/test_dank2.ts b/test_dank2.ts new file mode 100644 index 0000000..3af8a0c --- /dev/null +++ b/test_dank2.ts @@ -0,0 +1,27 @@ +import { prepareStream, Encoders } from "@dank074/discord-video-stream"; +import { demux } from "@dank074/discord-video-stream/dist/media/LibavDemuxer.js"; + +async function run() { + console.log("Starting prepareStream..."); + const { command, output } = prepareStream("https://samplelib.com/preview/mp4/sample-5s.mp4", { + encoder: Encoders.software(), + width: 1280, + height: 720, + includeAudio: true, + minimizeLatency: false // Add this + }); + + try { + const { video, audio } = await demux(output, { format: "nut" }); + console.log("DEMUX VIDEO:", !!video); + console.log("DEMUX AUDIO:", !!audio); + } catch(e) { + console.error("DEMUX ERR:", e); + } + + setTimeout(() => { + try { command.kill("SIGKILL"); } catch(e) {} + process.exit(0); + }, 10000); +} +run(); diff --git a/test_out.nut b/test_out.nut new file mode 100644 index 0000000..52d21b0 Binary files /dev/null and b/test_out.nut differ diff --git a/test_stream.ts b/test_stream.ts new file mode 100644 index 0000000..db22573 --- /dev/null +++ b/test_stream.ts @@ -0,0 +1,18 @@ +import { prepareStream } from "@dank074/discord-video-stream"; +import { demux } from "@dank074/discord-video-stream/dist/media/LibavDemuxer.js"; +import { Encoders } from "@dank074/discord-video-stream/dist/media/encoders/index.js"; + +async function run() { + const { command, output } = prepareStream("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", { + encoder: Encoders.software(), + width: 1280, + height: 720, + includeAudio: true + }); + + const { video, audio } = await demux(output, { format: "nut" }); + console.log("Video found:", !!video); + console.log("Audio found:", !!audio); + process.exit(0); +} +run(); diff --git a/tests/media/mediaController.test.ts b/tests/media/mediaController.test.ts index dbb3230..37e332a 100644 --- a/tests/media/mediaController.test.ts +++ b/tests/media/mediaController.test.ts @@ -200,7 +200,7 @@ describe("MediaController", () => { }); }); - it("starts screen share mode without resolving music source", async () => { + it("starts screen share mode by resolving the video source", async () => { const screenPlayback = deferred(); const screenController: ScreenShareController = { isActive: vi.fn(() => false), @@ -209,7 +209,7 @@ describe("MediaController", () => { stop: vi.fn(), })), }; - const resolveMediaSource = vi.fn(async (input) => source(input)); + const resolveMediaSource = vi.fn(async (input, mode) => source(input)); const controller = new MediaController({ isVoiceConnected: () => true, isBrowserStreaming: () => false, @@ -225,7 +225,7 @@ describe("MediaController", () => { expect(screenController.start).toHaveBeenCalledWith( "https://youtu.be/video", ); - expect(resolveMediaSource).not.toHaveBeenCalled(); + expect(resolveMediaSource).toHaveBeenCalledWith("https://youtu.be/video", "screen"); expect(state).toMatchObject({ playing: true, activeMode: "screen" }); });