streamlink/src/streamlink/stream/ffmpegmux.py

171 lines
5.6 KiB
Python
Raw Normal View History

import logging
import os
import random
import subprocess
import sys
import threading
from shutil import which
2017-02-06 11:41:12 +01:00
from streamlink import StreamError
from streamlink.compat import devnull
from streamlink.stream.stream import Stream, StreamIO
from streamlink.utils import NamedPipe
log = logging.getLogger(__name__)
class MuxedStream(Stream):
__shortname__ = "muxed-stream"
def __init__(self, session, *substreams, **options):
2020-10-30 19:06:48 +01:00
super().__init__(session)
self.substreams = substreams
self.subtitles = options.pop("subtitles", {})
self.options = options
def open(self):
fds = []
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):
log.debug("Opening {0} substream".format(substream.shortname()))
if update_maps:
maps.append(len(fds))
fds.append(substream and substream.open())
for i, subtitle in enumerate(self.subtitles.items()):
language, substream = subtitle
log.debug("Opening {0} subtitle stream".format(substream.shortname()))
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
return FFMPEGMuxer(self.session, *fds, **self.options).open()
@classmethod
def is_usable(cls, session):
return FFMPEGMuxer.is_usable(session)
class FFMPEGMuxer(StreamIO):
__commands__ = ['ffmpeg', 'ffmpeg.exe', 'avconv', 'avconv.exe']
@staticmethod
def copy_to_pipe(self, stream, pipe):
log.debug("Starting copy to pipe: {0}".format(pipe.path))
pipe.open("wb")
while not stream.closed:
try:
data = stream.read(8192)
if len(data):
pipe.write(data)
else:
break
except IOError:
log.error("Pipe copy aborted: {0}".format(pipe.path))
return
try:
pipe.close()
except IOError: # might fail closing, but that should be ok for the pipe
pass
log.debug("Pipe copy complete: {0}".format(pipe.path))
def __init__(self, session, *streams, **options):
if not self.is_usable(session):
raise StreamError("cannot use FFMPEG")
self.session = session
self.process = None
self.streams = streams
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]
self.pipe_threads = [threading.Thread(target=self.copy_to_pipe, args=(self, stream, np))
for stream, np in
zip(self.streams, self.pipes)]
ofmt = options.pop("format", "matroska")
outpath = options.pop("outpath", "pipe:1")
videocodec = session.options.get("ffmpeg-video-transcode") or options.pop("vcodec", "copy")
audiocodec = session.options.get("ffmpeg-audio-transcode") or options.pop("acodec", "copy")
metadata = options.pop("metadata", {})
maps = options.pop("maps", [])
MPEG DASH Support (initial) (#1637) * stream.dash: parser for dash manifest files * stream.dash: stream player for dash with plugin to support dash:// prefixed urls * cli.main: make sure that streams are closed on errors * stream.dash: fix some parsing bugs * stream.dash: tidy up the segment number generation * plugins.dash: wip segment timeline * stream.dash: update to segment timeline parsing * stream.dash: py3 support * stream.dash: raise an error for DRM protected streams * stream.dash: fixes for timescaling and some segment templates * docs: add DASHStream to docs with other Stream classes * dash: fix for video only stream * plugins.dash: fix bug where all URLs were matched * stream.dash: fix issue with manifest reload * plugin.dash: add tests and fix a couple of bugs found in testing * stream.dash: add some tests to cover the DASHStream classes * WIP: audio only streams * add some debugging for threads and remove the thread joins * dash: startNumber should default to 1 * dash: follow redirects to get the base url * dash: fix bool parser, and segment template parser * dash: fixed some issues... ...with some segment templates, as well as improving the presentation delay handling * dash: add a back-off for checking for manifest changes * dash: fix broken tests * dash: incomplete support for Segment@r * dash: fixed audio/video sync issue Added a `copyts` option to the FFMPEG muxer class so that the timestamps given in the source files are maintained, this appears to fix the a/v sync issues. NB. The timestamp can get weird, but that's how it is :) * dash: support for Time _and_ Number in segment timeline * tests: add some dash parser tests + a little refactor * tests: add dash to built in plugins * tests: more coverage of dash_parser Added a new module for tests, `freezegun`, for mocking time. * dash: fix for missing publishTime * dash: update available_at times to be datetime This should fix any timezone or leap-second issues, etc. * fixed timing issue for 1tv.ru * dash: fix availability timeline for segment timeline * dash: flake8 tweaks * dash: add a few debug logging messages
2018-05-30 21:30:38 +02:00
copyts = options.pop("copyts", False)
self._cmd = [self.command(session), '-nostats', '-y']
for np in self.pipes:
self._cmd.extend(["-i", np.path])
self._cmd.extend(['-c:v', videocodec])
self._cmd.extend(['-c:a', audiocodec])
for m in maps:
self._cmd.extend(["-map", str(m)])
MPEG DASH Support (initial) (#1637) * stream.dash: parser for dash manifest files * stream.dash: stream player for dash with plugin to support dash:// prefixed urls * cli.main: make sure that streams are closed on errors * stream.dash: fix some parsing bugs * stream.dash: tidy up the segment number generation * plugins.dash: wip segment timeline * stream.dash: update to segment timeline parsing * stream.dash: py3 support * stream.dash: raise an error for DRM protected streams * stream.dash: fixes for timescaling and some segment templates * docs: add DASHStream to docs with other Stream classes * dash: fix for video only stream * plugins.dash: fix bug where all URLs were matched * stream.dash: fix issue with manifest reload * plugin.dash: add tests and fix a couple of bugs found in testing * stream.dash: add some tests to cover the DASHStream classes * WIP: audio only streams * add some debugging for threads and remove the thread joins * dash: startNumber should default to 1 * dash: follow redirects to get the base url * dash: fix bool parser, and segment template parser * dash: fixed some issues... ...with some segment templates, as well as improving the presentation delay handling * dash: add a back-off for checking for manifest changes * dash: fix broken tests * dash: incomplete support for Segment@r * dash: fixed audio/video sync issue Added a `copyts` option to the FFMPEG muxer class so that the timestamps given in the source files are maintained, this appears to fix the a/v sync issues. NB. The timestamp can get weird, but that's how it is :) * dash: support for Time _and_ Number in segment timeline * tests: add some dash parser tests + a little refactor * tests: add dash to built in plugins * tests: more coverage of dash_parser Added a new module for tests, `freezegun`, for mocking time. * dash: fix for missing publishTime * dash: update available_at times to be datetime This should fix any timezone or leap-second issues, etc. * fixed timing issue for 1tv.ru * dash: fix availability timeline for segment timeline * dash: flake8 tweaks * dash: add a few debug logging messages
2018-05-30 21:30:38 +02:00
if copyts:
self._cmd.extend(["-copyts"])
self._cmd.extend(["-start_at_zero"])
MPEG DASH Support (initial) (#1637) * stream.dash: parser for dash manifest files * stream.dash: stream player for dash with plugin to support dash:// prefixed urls * cli.main: make sure that streams are closed on errors * stream.dash: fix some parsing bugs * stream.dash: tidy up the segment number generation * plugins.dash: wip segment timeline * stream.dash: update to segment timeline parsing * stream.dash: py3 support * stream.dash: raise an error for DRM protected streams * stream.dash: fixes for timescaling and some segment templates * docs: add DASHStream to docs with other Stream classes * dash: fix for video only stream * plugins.dash: fix bug where all URLs were matched * stream.dash: fix issue with manifest reload * plugin.dash: add tests and fix a couple of bugs found in testing * stream.dash: add some tests to cover the DASHStream classes * WIP: audio only streams * add some debugging for threads and remove the thread joins * dash: startNumber should default to 1 * dash: follow redirects to get the base url * dash: fix bool parser, and segment template parser * dash: fixed some issues... ...with some segment templates, as well as improving the presentation delay handling * dash: add a back-off for checking for manifest changes * dash: fix broken tests * dash: incomplete support for Segment@r * dash: fixed audio/video sync issue Added a `copyts` option to the FFMPEG muxer class so that the timestamps given in the source files are maintained, this appears to fix the a/v sync issues. NB. The timestamp can get weird, but that's how it is :) * dash: support for Time _and_ Number in segment timeline * tests: add some dash parser tests + a little refactor * tests: add dash to built in plugins * tests: more coverage of dash_parser Added a new module for tests, `freezegun`, for mocking time. * dash: fix for missing publishTime * dash: update available_at times to be datetime This should fix any timezone or leap-second issues, etc. * fixed timing issue for 1tv.ru * dash: fix availability timeline for segment timeline * dash: flake8 tweaks * dash: add a few debug logging messages
2018-05-30 21:30:38 +02:00
for stream, data in metadata.items():
for datum in data:
self._cmd.extend(["-metadata:{0}".format(stream), datum])
self._cmd.extend(['-f', ofmt, outpath])
log.debug("ffmpeg command: {0}".format(' '.join(self._cmd)))
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()
self.process = subprocess.Popen(self._cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=self.errorlog)
return self
@classmethod
def is_usable(cls, session):
return cls.command(session) is not None
@classmethod
def command(cls, session):
command = []
if session.options.get("ffmpeg-ffmpeg"):
command.append(session.options.get("ffmpeg-ffmpeg"))
for cmd in command or cls.__commands__:
if which(cmd):
return cmd
def read(self, size=-1):
data = self.process.stdout.read(size)
return data
def close(self):
log.debug("Closing ffmpeg thread")
if self.process:
# kill ffmpeg
self.process.kill()
self.process.stdout.close()
# close the streams
for stream in self.streams:
if hasattr(stream, "close"):
stream.close()
log.debug("Closed all the substreams")
if self.close_errorlog:
self.errorlog.close()
self.errorlog = None