3232import subprocess
3333import shlex
3434import threading
35- from typing import IO , TYPE_CHECKING , Any , Literal , TypeVar
35+ from typing import IO , TYPE_CHECKING , Any , Literal , TypeVar , overload
3636
37+ from discord .file import File
3738from discord .utils import MISSING , SequenceProxy
3839from discord .player import FFmpegAudio
3940
41+ from .errors import FFmpegNotFound
42+
4043if TYPE_CHECKING :
4144 from typing_extensions import ParamSpec , Self
4245
5255__all__ = (
5356 "Sink" ,
5457 "RawData" ,
58+ "FFmpegSink" ,
59+ "FilterSink" ,
60+ "MultiSink" ,
5561)
5662
5763
@@ -309,20 +315,77 @@ def __init__(self, **kwargs: Any) -> None:
309315 raise DeprecationWarning ("RawData has been deprecated in favour of VoiceData" )
310316
311317
312- class _FFmpegSink (Sink ):
318+ class FFmpegSink (Sink ):
319+ """A :class:`Sink` built to use ffmpeg executables.
320+
321+ You can find default implementations of this sink in:
322+
323+ - :class:`M4ASink`
324+ - :class:`MKASink`
325+
326+ .. versionadded:: 2.7
327+
328+ Parameters
329+ ----------
330+ filename: :class:`str`
331+ The file in which the ffmpeg buffer should be saved to.
332+ Can not be mixed with ``buffer``.
333+ buffer: IO[:class:`bytes`]
334+ The buffer in which the ffmpeg result would be written to.
335+ Can not be mixed with ``filename``.
336+ executable: :class:`str`
337+ The executable in which ``ffmpeg`` is in.
338+ stderr: IO[:class:`bytes`] | :data:`None`
339+ The stderr buffer in whcih will be written. Defaults to ``None``.
340+ before_options: :class:`str` | :data:`None`
341+ The options to append **before** the default ones.
342+ options: :class:`str` | :data:`None`
343+ The options to append **after** the default ones. You can override the
344+ default ones with this.
345+ error_hook: Callable[[:class:`FFmpegSink`, :class:`Exception`, :class:`discord.voice.VoiceData` | :data:`None`], Any] | :data:`None`
346+ The callback to call when an error ocurrs with this sink.
347+ """
348+
349+ @overload
350+ def __init__ (
351+ self ,
352+ * ,
353+ filename : str ,
354+ executable : str = ...,
355+ stderr : IO [bytes ] = ...,
356+ before_options : str | None = ...,
357+ options : str | None = ...,
358+ error_hook : Callable [[Self , Exception , VoiceData | None ], Any ] | None = ...,
359+ ) -> None : ...
360+
361+ @overload
362+ def __init__ (
363+ self ,
364+ * ,
365+ buffer : IO [bytes ],
366+ executable : str = ...,
367+ stderr : IO [bytes ] = ...,
368+ before_options : str | None = ...,
369+ options : str | None = ...,
370+ error_hook : Callable [[Self , Exception , VoiceData | None ], Any ] | None = ...,
371+ ) -> None : ...
372+
313373 def __init__ (
314374 self ,
315375 * ,
316376 filename : str = MISSING ,
317377 buffer : IO [bytes ] = MISSING ,
318- executable : str = ' ffmpeg' ,
378+ executable : str = " ffmpeg" ,
319379 stderr : IO [bytes ] | None = None ,
320380 before_options : str | None = None ,
321381 options : str | None = None ,
322382 error_hook : Callable [[Self , Exception , VoiceData | None ], Any ] | None = None ,
323383 ) -> None :
324384 super ().__init__ ()
325385
386+ if filename is not MISSING and buffer is not MISSING :
387+ raise TypeError ("can't mix filename and buffer parameters" )
388+
326389 self .filename : str = filename or "pipe:1"
327390 self .buffer : IO [bytes ] = buffer
328391
@@ -345,7 +408,7 @@ def __init__(
345408 args .extend (shlex .split (before_options ))
346409
347410 args .extend ({
348- "-f" : "s161e " ,
411+ "-f" : "s16le " ,
349412 "-ar" : "48000" ,
350413 "-ac" : "2" ,
351414 "-i" : "pipe:0" ,
@@ -382,7 +445,7 @@ def __init__(
382445 self ._stderr_reader_thread .start ()
383446
384447 @staticmethod
385- def _on_error (_self : _FFmpegSink , error : Exception , data : VoiceData | None ) -> None :
448+ def _on_error (_self : FFmpegSink , error : Exception , data : VoiceData | None ) -> None :
386449 _self .client .stop_recording () # type: ignore
387450
388451 def is_opus (self ) -> bool :
@@ -404,6 +467,21 @@ def write(self, user: User | Member | None, data: VoiceData) -> None:
404467 self ._kill_processes ()
405468 self .on_error (self , exc , data )
406469
470+
471+ def to_file (self , filename : str , / , * , description : str | None = None , spoiler : bool = False ) -> File | None :
472+ """Returns the :class:`discord.File` of this sink.
473+
474+ This is only applicable if this sink uses a ``buffer`` instead of a ``filename``.
475+
476+ .. warning::
477+
478+ This should be used only after the sink has stopped recording.
479+ """
480+ if self .buffer is not MISSING :
481+ fp = File (self .buffer .read (), filename = filename , description = description , spoiler = spoiler )
482+ return fp
483+ return None
484+
407485 def _spawn_process (self , args : Any , ** subprocess_kwargs : Any ) -> subprocess .Popen :
408486 _log .debug ("Spawning ffmpeg process with command %s and kwargs %s" , args , subprocess_kwargs )
409487 process = None
@@ -412,7 +490,7 @@ def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Pope
412490 process = subprocess .Popen (args , creationflags = CREATE_NO_WINDOW , ** subprocess_kwargs )
413491 except FileNotFoundError :
414492 executable = args .partition (' ' )[0 ] if isinstance (args , str ) else args [0 ]
415- raise Exception (f"{ executable !r} executable was not found" ) from None
493+ raise FFmpegNotFound (f"{ executable !r} executable was not found" ) from None
416494 except subprocess .SubprocessError as exc :
417495 raise Exception (f"Popen failed: { exc .__class__ .__name__ } : { exc } " ) from exc
418496 else :
0 commit comments