1
Fork 0
mirror of https://github.com/Steffo99/bbbdl.git synced 2024-11-22 07:44:18 +00:00

🐞 Fix slides scaling (#1)

This commit is contained in:
Steffo 2020-11-15 12:46:17 +01:00 committed by GitHub
parent b8a2656997
commit 85cecfcc40
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 47 deletions

View file

@ -30,13 +30,13 @@ def download(input_url, output_file, overwrite=False, verbose_ffmpeg=False, debu
meeting = Meeting.from_url(input_url) meeting = Meeting.from_url(input_url)
click.secho(f"Downloading: {input_url} -> {output_file}", err=True, fg="green") click.secho(f"Downloading: {input_url} -> {output_file}", err=True, fg="green")
streams = compose_lesson(meeting) tracks = compose_lesson(meeting, 1280, 720)
output = ffmpeg.output(*streams, output_file) output = ffmpeg.output(tracks.video, tracks.audio, output_file)
if debug: if debug:
click.echo(" ".join(output.compile())) click.echo(" ".join(output.compile()))
else:
output.run(quiet=not verbose_ffmpeg, overwrite_output=True) output.run(quiet=not verbose_ffmpeg, overwrite_output=True)

View file

@ -1,24 +1,30 @@
from typing import * from typing import *
import ffmpeg import ffmpeg
from .resources import Meeting from .resources import Meeting
from .tracks import Tracks
def compose_screensharing(meeting: Meeting) -> Tuple[ffmpeg.Stream, ffmpeg.Stream]: def compose_screensharing(meeting: Meeting, width: int, height: int) -> Tracks:
"""Keep the deskshare video and the webcam audio, while discarding the rest.""" tracks = Tracks()
return ( if meeting.webcams:
meeting.deskshare.as_stream().video, tracks.overlay(meeting.webcams.get_video().filter("scale", width, height).filter("setsar", 1, 1))
meeting.webcams.as_stream().audio, tracks.amerge(meeting.webcams.get_audio())
)
if meeting.deskshare:
tracks.overlay(meeting.deskshare.get_video().filter("scale", width, height).filter("setsar", 1, 1))
return tracks
def compose_lesson(meeting: Meeting) -> Tuple[ffmpeg.Stream, ffmpeg.Stream]: def compose_lesson(meeting: Meeting, width: int, height: int) -> Tracks:
"""Keep slides, deskshare video and webcam audio, while discarding the rest.""" tracks = compose_screensharing(meeting, width, height)
video_stream, audio_stream = compose_screensharing(meeting)
for shape in meeting.shapes: for shape in meeting.shapes:
video_stream = ffmpeg.overlay(video_stream, shape.resource.as_stream().video.filter("scale", 1280, 720), scaled_split_shape = shape.resource.get_video().filter("scale", width, height).filter("setsar", 1, 1).split()
enable=f"between(t, {shape.start}, {shape.end})") count = 0
for enable in shape.enables:
count += 1
tracks.overlay(scaled_split_shape.stream(count), enable=f"between(t, {enable[0]}, {enable[1]})")
return video_stream, audio_stream return tracks

View file

@ -7,35 +7,42 @@ import ffmpeg
from .urlhandler import playback_to_data from .urlhandler import playback_to_data
@dataclasses.dataclass()
class Resource: class Resource:
href: str def __init__(self, href: str):
self.href: str = href
self._video: Optional[ffmpeg.Stream] = None
self._video_count: int = 0
self._audio: Optional[ffmpeg.Stream] = None
self._audio_count: int = 0
def as_stream(self, **kwargs) -> ffmpeg.Stream: def __repr__(self):
return ffmpeg.input(self.href, **kwargs) return f"<{self.__class__.__qualname__} href={self.href}>"
@classmethod
def check_and_create(cls, href: str) -> Optional[Resource]:
"""Check if the resource exists, and create it if it does."""
r = requests.head(href)
if not (200 <= r.status_code < 400):
return None
return cls(href=href)
def get_audio(self) -> ffmpeg.nodes.FilterableStream:
if self._audio is None:
self._audio = ffmpeg.input(self.href).audio.asplit()
self._audio_count += 1
return self._audio.stream(self._audio_count)
def get_video(self) -> ffmpeg.nodes.FilterableStream:
if self._video is None:
self._video = ffmpeg.input(self.href).video.split()
self._video_count += 1
return self._video.stream(self._video_count)
@dataclasses.dataclass() @dataclasses.dataclass()
class Shape: class Shape:
resource: Resource resource: Resource
start: float enables: List[Tuple[float, float]]
end: float
@classmethod
def from_tag(cls, tag: bs4.Tag, *, base_url: str) -> Shape:
# No, `"in" not in tag` does not work
if not tag["in"]:
raise ValueError("Tag has no 'in' parameter")
if not tag["out"]:
raise ValueError("Tag has no 'out' parameter")
if not tag["xlink:href"]:
raise ValueError("Tag has no 'xlink:href' parameter")
return cls(
resource=Resource(href=f"{base_url}/{tag['xlink:href']}"),
start=float(tag["in"]),
end=float(tag["out"]),
)
@dataclasses.dataclass() @dataclasses.dataclass()
@ -49,22 +56,30 @@ class Meeting:
r = requests.get(f"{base_url}/presentation/{meeting_id}/metadata.xml") r = requests.get(f"{base_url}/presentation/{meeting_id}/metadata.xml")
r.raise_for_status() r.raise_for_status()
deskshare = Resource(href=f"{base_url}/presentation/{meeting_id}/deskshare/deskshare.webm") deskshare = Resource.check_and_create(href=f"{base_url}/presentation/{meeting_id}/deskshare/deskshare.mp4")
webcams = Resource(href=f"{base_url}/presentation/{meeting_id}/video/webcams.mp4") webcams = Resource.check_and_create(href=f"{base_url}/presentation/{meeting_id}/video/webcams.mp4")
shape_soup = bs4.BeautifulSoup(requests.get(f"{base_url}/presentation/{meeting_id}/shapes.svg").text, shape_soup = bs4.BeautifulSoup(requests.get(f"{base_url}/presentation/{meeting_id}/shapes.svg").text,
"lxml") "lxml")
shapes: List[Shape] = []
shapes: Dict[str, Shape] = {}
for tag in shape_soup.find_all("image"): for tag in shape_soup.find_all("image"):
try: if not tag["in"]:
shapes.append(Shape.from_tag(tag, base_url=f"{base_url}/presentation/{meeting_id}")) raise ValueError("Tag has no 'in' parameter")
except ValueError: if not tag["out"]:
continue raise ValueError("Tag has no 'out' parameter")
if not tag["xlink:href"]:
raise ValueError("Tag has no 'xlink:href' parameter")
url = tag["xlink:href"]
if url not in shapes:
shapes[url] = Shape(resource=Resource(f"{base_url}/presentation/{meeting_id}/{url}"), enables=[])
shapes[url].enables.append((tag["in"], tag["out"]))
return cls( return cls(
deskshare=deskshare, deskshare=deskshare,
webcams=webcams, webcams=webcams,
shapes=shapes shapes=list(shapes.values())
) )
@classmethod @classmethod

20
bbbdl/tracks.py Normal file
View file

@ -0,0 +1,20 @@
from typing import Optional
import ffmpeg.nodes
class Tracks:
def __init__(self):
self.video: Optional[ffmpeg.nodes.FilterableStream] = None
self.audio: Optional[ffmpeg.nodes.FilterableStream] = None
def overlay(self, other: ffmpeg.nodes.FilterableStream, **kwargs):
if self.video is None:
self.video = other
else:
self.video = self.video.overlay(other, **kwargs)
def amerge(self, other: ffmpeg.nodes.FilterableStream, **kwargs):
if self.audio is None:
self.audio = other
else:
self.audio = ffmpeg.filter((self.audio, other), "amerge", **kwargs)