2020-10-07 14:45:54 +02:00
|
|
|
import logging
|
2017-01-10 18:44:32 +01:00
|
|
|
import os
|
|
|
|
import random
|
|
|
|
import subprocess
|
|
|
|
import sys
|
2020-10-07 14:45:54 +02:00
|
|
|
import threading
|
2020-10-26 14:46:17 +01:00
|
|
|
from shutil import which
|
2017-02-06 11:41:12 +01:00
|
|
|
|
|
|
|
from streamlink import StreamError
|
2020-10-07 14:45:54 +02:00
|
|
|
from streamlink.compat import devnull
|
2020-10-26 14:46:17 +01:00
|
|
|
from streamlink.stream.stream import Stream, StreamIO
|
|
|
|
from streamlink.utils import NamedPipe
|
2018-05-30 03:15:11 +02:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
2017-01-10 18:44:32 +01:00
|
|
|
|
2017-02-21 18:37:59 +01:00
|
|
|
|
2017-01-10 18:44:32 +01:00
|
|
|
class MuxedStream(Stream):
|
|
|
|
__shortname__ = "muxed-stream"
|
|
|
|
|
|
|
|
def __init__(self, session, *substreams, **options):
|
2020-10-30 19:06:48 +01:00
|
|
|
super().__init__(session)
|
2017-01-10 18:44:32 +01:00
|
|
|
self.substreams = substreams
|
2017-02-27 18:20:17 +01:00
|
|
|
self.subtitles = options.pop("subtitles", {})
|
2017-01-10 18:44:32 +01:00
|
|
|
self.options = options
|
|
|
|
|
|
|
|
def open(self):
|
|
|
|
fds = []
|
2017-02-27 18:20:17 +01:00
|
|
|
metadata = self.options.get("metadata", {})
|
|
|
|
maps = self.options.get("maps", [])
|
|
|
|
# only update the maps values if they haven't been set
|
|
|
|
update_maps = not maps
|
|
|
|
for i, substream in enumerate(self.substreams):
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("Opening {0} substream".format(substream.shortname()))
|
2017-02-27 18:20:17 +01:00
|
|
|
if update_maps:
|
|
|
|
maps.append(len(fds))
|
2017-01-20 15:20:43 +01:00
|
|
|
fds.append(substream and substream.open())
|
2017-02-27 18:20:17 +01:00
|
|
|
|
|
|
|
for i, subtitle in enumerate(self.subtitles.items()):
|
|
|
|
language, substream = subtitle
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("Opening {0} subtitle stream".format(substream.shortname()))
|
2017-02-27 18:20:17 +01:00
|
|
|
if update_maps:
|
|
|
|
maps.append(len(fds))
|
|
|
|
fds.append(substream and substream.open())
|
|
|
|
metadata["s:s:{0}".format(i)] = ["language={0}".format(language)]
|
|
|
|
|
|
|
|
self.options["metadata"] = metadata
|
|
|
|
self.options["maps"] = maps
|
|
|
|
|
2017-01-10 18:44:32 +01:00
|
|
|
return FFMPEGMuxer(self.session, *fds, **self.options).open()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def is_usable(cls, session):
|
|
|
|
return FFMPEGMuxer.is_usable(session)
|
|
|
|
|
|
|
|
|
2017-01-27 18:35:24 +01:00
|
|
|
class FFMPEGMuxer(StreamIO):
|
2017-01-12 13:36:30 +01:00
|
|
|
__commands__ = ['ffmpeg', 'ffmpeg.exe', 'avconv', 'avconv.exe']
|
2020-11-23 18:29:26 +01:00
|
|
|
DEFAULT_OUTPUT_FORMAT = "matroska"
|
|
|
|
DEFAULT_VIDEO_CODEC = "copy"
|
|
|
|
DEFAULT_AUDIO_CODEC = "copy"
|
2017-01-10 18:44:32 +01:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def copy_to_pipe(self, stream, pipe):
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("Starting copy to pipe: {0}".format(pipe.path))
|
2017-01-10 18:44:32 +01:00
|
|
|
pipe.open("wb")
|
|
|
|
while not stream.closed:
|
|
|
|
try:
|
|
|
|
data = stream.read(8192)
|
|
|
|
if len(data):
|
|
|
|
pipe.write(data)
|
|
|
|
else:
|
|
|
|
break
|
2020-11-26 22:39:22 +01:00
|
|
|
except OSError:
|
2018-05-30 03:15:11 +02:00
|
|
|
log.error("Pipe copy aborted: {0}".format(pipe.path))
|
2017-01-10 18:44:32 +01:00
|
|
|
return
|
|
|
|
try:
|
|
|
|
pipe.close()
|
2020-11-26 22:39:22 +01:00
|
|
|
except OSError: # might fail closing, but that should be ok for the pipe
|
2017-01-10 18:44:32 +01:00
|
|
|
pass
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("Pipe copy complete: {0}".format(pipe.path))
|
2017-01-10 18:44:32 +01:00
|
|
|
|
|
|
|
def __init__(self, session, *streams, **options):
|
|
|
|
if not self.is_usable(session):
|
2017-01-20 15:20:43 +01:00
|
|
|
raise StreamError("cannot use FFMPEG")
|
2017-01-10 18:44:32 +01:00
|
|
|
|
|
|
|
self.session = session
|
|
|
|
self.process = None
|
|
|
|
self.streams = streams
|
2017-01-20 15:20:43 +01:00
|
|
|
|
2017-02-06 11:41:12 +01:00
|
|
|
self.pipes = [NamedPipe("ffmpeg-{0}-{1}".format(os.getpid(), random.randint(0, 1000))) for _ in self.streams]
|
2017-01-10 18:44:32 +01:00
|
|
|
self.pipe_threads = [threading.Thread(target=self.copy_to_pipe, args=(self, stream, np))
|
|
|
|
for stream, np in
|
2017-01-20 15:20:43 +01:00
|
|
|
zip(self.streams, self.pipes)]
|
2017-01-10 18:44:32 +01:00
|
|
|
|
2020-11-23 18:29:26 +01:00
|
|
|
ofmt = session.options.get("ffmpeg-fout") or options.pop("format", self.DEFAULT_OUTPUT_FORMAT)
|
2017-01-10 18:44:32 +01:00
|
|
|
outpath = options.pop("outpath", "pipe:1")
|
2020-11-23 18:29:26 +01:00
|
|
|
videocodec = session.options.get("ffmpeg-video-transcode") or options.pop("vcodec", self.DEFAULT_VIDEO_CODEC)
|
|
|
|
audiocodec = session.options.get("ffmpeg-audio-transcode") or options.pop("acodec", self.DEFAULT_AUDIO_CODEC)
|
2017-01-20 15:20:43 +01:00
|
|
|
metadata = options.pop("metadata", {})
|
2017-02-21 18:37:59 +01:00
|
|
|
maps = options.pop("maps", [])
|
2018-05-30 21:30:38 +02:00
|
|
|
copyts = options.pop("copyts", False)
|
2020-12-15 16:57:50 +01:00
|
|
|
start_at_zero = session.options.get("ffmpeg-start-at-zero") or options.pop("start_at_zero", False)
|
2017-01-10 18:44:32 +01:00
|
|
|
|
|
|
|
self._cmd = [self.command(session), '-nostats', '-y']
|
|
|
|
for np in self.pipes:
|
|
|
|
self._cmd.extend(["-i", np.path])
|
|
|
|
|
2017-01-20 15:20:43 +01:00
|
|
|
self._cmd.extend(['-c:v', videocodec])
|
|
|
|
self._cmd.extend(['-c:a', audiocodec])
|
|
|
|
|
2017-02-21 18:37:59 +01:00
|
|
|
for m in maps:
|
|
|
|
self._cmd.extend(["-map", str(m)])
|
|
|
|
|
2018-05-30 21:30:38 +02:00
|
|
|
if copyts:
|
|
|
|
self._cmd.extend(["-copyts"])
|
2020-11-23 18:29:26 +01:00
|
|
|
if start_at_zero:
|
|
|
|
self._cmd.extend(["-start_at_zero"])
|
2018-05-30 21:30:38 +02:00
|
|
|
|
2017-01-20 15:20:43 +01:00
|
|
|
for stream, data in metadata.items():
|
|
|
|
for datum in data:
|
2020-11-23 18:29:26 +01:00
|
|
|
stream_id = ":{0}".format(stream) if stream else ""
|
|
|
|
self._cmd.extend(["-metadata{0}".format(stream_id), datum])
|
2017-01-20 15:20:43 +01:00
|
|
|
|
|
|
|
self._cmd.extend(['-f', ofmt, outpath])
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("ffmpeg command: {0}".format(' '.join(self._cmd)))
|
2017-01-10 18:44:32 +01:00
|
|
|
self.close_errorlog = False
|
|
|
|
|
|
|
|
if session.options.get("ffmpeg-verbose"):
|
|
|
|
self.errorlog = sys.stderr
|
|
|
|
elif session.options.get("ffmpeg-verbose-path"):
|
|
|
|
self.errorlog = open(session.options.get("ffmpeg-verbose-path"), "w")
|
|
|
|
self.close_errorlog = True
|
|
|
|
else:
|
|
|
|
self.errorlog = devnull()
|
|
|
|
|
|
|
|
def open(self):
|
|
|
|
for t in self.pipe_threads:
|
|
|
|
t.daemon = True
|
|
|
|
t.start()
|
2018-02-22 16:41:23 +01:00
|
|
|
self.process = subprocess.Popen(self._cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=self.errorlog)
|
2017-01-10 18:44:32 +01:00
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def is_usable(cls, session):
|
|
|
|
return cls.command(session) is not None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def command(cls, session):
|
2017-01-12 13:36:30 +01:00
|
|
|
command = []
|
|
|
|
if session.options.get("ffmpeg-ffmpeg"):
|
|
|
|
command.append(session.options.get("ffmpeg-ffmpeg"))
|
2017-01-10 18:44:32 +01:00
|
|
|
for cmd in command or cls.__commands__:
|
2017-01-27 18:35:24 +01:00
|
|
|
if which(cmd):
|
2017-01-10 18:44:32 +01:00
|
|
|
return cmd
|
|
|
|
|
|
|
|
def read(self, size=-1):
|
|
|
|
data = self.process.stdout.read(size)
|
|
|
|
return data
|
|
|
|
|
|
|
|
def close(self):
|
2020-11-22 20:13:31 +01:00
|
|
|
if self.closed:
|
|
|
|
return
|
|
|
|
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("Closing ffmpeg thread")
|
2017-01-10 18:44:32 +01:00
|
|
|
if self.process:
|
|
|
|
# kill ffmpeg
|
|
|
|
self.process.kill()
|
|
|
|
self.process.stdout.close()
|
|
|
|
|
|
|
|
# close the streams
|
|
|
|
for stream in self.streams:
|
2020-11-22 20:13:31 +01:00
|
|
|
if hasattr(stream, "close") and callable(stream.close):
|
2017-01-10 18:44:32 +01:00
|
|
|
stream.close()
|
|
|
|
|
2018-05-30 03:15:11 +02:00
|
|
|
log.debug("Closed all the substreams")
|
2020-11-22 20:13:31 +01:00
|
|
|
|
2017-01-10 18:44:32 +01:00
|
|
|
if self.close_errorlog:
|
|
|
|
self.errorlog.close()
|
|
|
|
self.errorlog = None
|
2020-11-22 20:13:31 +01:00
|
|
|
|
|
|
|
super().close()
|