1
0
Fork 0
mirror of https://github.com/Findus23/BBBtoVideo.git synced 2024-09-18 12:53:45 +02:00

initial version

This commit is contained in:
Lukas Winkler 2020-11-05 19:49:59 +01:00
commit 7064bf4460
Signed by: lukas
GPG key ID: 54DE4D798D244853
7 changed files with 225 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.idea/
*.mp4
data/
config.py

21
config.sample.py Normal file
View file

@ -0,0 +1,21 @@
from pathlib import Path
playback_url = "https://bbb.example.com/"
meeting_id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-aaaaaaaaaaaaa"
data_dir = Path("./data")
fps = 5 # frames per second
pointer_size = 100
start = 10 # in seconds, set to None to start from the beginning
end = 100 # in seconds, set to None to convert until the end of the video
hide_pointer_if_offscreen = False
# set to "previous" or "next" for raw position data
# set to "linear" for linear interpolation
# set to "quadratic" for quadratic interpolation (needs hide_pointer_if_offscreen = False)
interpolation_method = "quadratic"

31
cursor.py Normal file
View file

@ -0,0 +1,31 @@
from pathlib import Path
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from scipy.interpolate import interp1d
from config import interpolation_method, hide_pointer_if_offscreen
class Cursor:
def __init__(self, xml_file: Path):
tree = ElementTree.parse(xml_file)
root = tree.getroot()
self.timestamps = []
self.xs = []
self.ys = []
child: Element
for child in root:
self.timestamps.append(float(child.attrib["timestamp"]))
cursor_text = child.find("cursor").text
x, y = list(map(float, cursor_text.split()))
if hide_pointer_if_offscreen:
if x < 0:
x = None
if y < 0:
y = None
self.xs.append(x)
self.ys.append(y)
self.xspline = interp1d(self.timestamps, self.xs, kind=interpolation_method)
self.yspline = interp1d(self.timestamps, self.ys, kind=interpolation_method)

28
download.py Normal file
View file

@ -0,0 +1,28 @@
from pathlib import Path
import requests
from config import data_dir, meeting_id, playback_url
def fetch_file(path: str, show_progress=False) -> Path:
local_file = data_dir / meeting_id / path
if local_file.exists():
return local_file
else:
print("downloading", path)
data_url = f"{playback_url}presentation/{meeting_id}/{path}"
local_file.parent.mkdir(parents=True, exist_ok=True)
r = requests.get(data_url)
r.raise_for_status()
file_size = int(r.headers.get("content-length"))
progress = 0
with local_file.open("wb") as fd:
for chunk in r.iter_content(chunk_size=128):
fd.write(chunk)
progress += 128
if show_progress:
print(f"Progress: {progress / file_size * 100:.2f}%", end="\r", flush=True)
if show_progress:
print() # flush new line
return local_file

81
main.py Normal file
View file

@ -0,0 +1,81 @@
import subprocess
from pathlib import Path
from typing import Optional
import cv2
import numpy as np
from config import fps, pointer_size, start, end
from cursor import Cursor
from download import fetch_file
from metadata import Metadata
from shapes import Shapes
metadata = Metadata(fetch_file("metadata.xml"))
print(f'found "{metadata.meetingName}"')
print(f"starting on {metadata.starttime}")
cursor = Cursor(fetch_file("cursor.xml"))
shapes = Shapes(fetch_file("shapes.svg"))
for slide in shapes.slides:
a = slide.file # pre-download slide images
audio = fetch_file("video/webcams.webm", show_progress=True)
print("start generating video")
if start is None:
time = 0
else:
time = start
if end is None:
end = metadata.duration
print(end)
frame_len = 1 / fps
slide_id = -1
video_file = Path("without_audio.mp4")
fourcc = cv2.VideoWriter_fourcc(*'MP4V')
print(shapes.maxwidth, shapes.maxheight)
out = cv2.VideoWriter(str(video_file), fourcc, fps, (shapes.maxwidth, shapes.maxheight))
slide = shapes.slides[0]
image: Optional[np.ndarray] = None
print()
while time <= end:
frame = np.zeros((shapes.maxheight, shapes.maxwidth, 3))
while time > slide.end or image is None:
slide_id += 1
slide = shapes.slides[slide_id]
image: np.ndarray = cv2.imread(str(slide.file))
frame[0:slide.height, 0:slide.width] = image
x_frac = cursor.xspline(time)
y_frac = cursor.yspline(time)
if not (np.isnan(x_frac) or np.isnan(y_frac)):
cursor_x = int(round(x_frac * slide.width))
cursor_y = int(round(y_frac * slide.height))
if -pointer_size <= cursor_x <= slide.width + pointer_size and -pointer_size <= cursor_y <= slide.height + pointer_size:
frame[cursor_y - pointer_size:cursor_y + pointer_size,
cursor_x - pointer_size:cursor_x + pointer_size] = np.array([0, 0, 255], dtype=np.uint8)
else:
cursor_x = None
cursor_y = None
print(f"{time:.2f}/{end:.2f} {slide_id} {cursor_x} {cursor_y} ", end="\r", flush=True)
out.write(frame.astype(np.uint8))
time += frame_len
out.release()
print()
command = [
"ffmpeg", "-i", str(video_file), "-ss", str(start), "-to", str(end), "-i", str(audio), "-ss", str(0), "-c",
"copy",
"output.mp4",
"-y"
]
print("merge video with audio")
print(" ".join(command))
subprocess.run(command)

14
metadata.py Normal file
View file

@ -0,0 +1,14 @@
from datetime import datetime
from pathlib import Path
from xml.etree import ElementTree
class Metadata:
def __init__(self, xml_file: Path):
tree = ElementTree.parse(xml_file)
root = tree.getroot()
self.meetingName = root.find("./meta/meetingName").text
self.starttime = datetime.fromtimestamp(int(root.find("./start_time").text) / 1000)
self.duration = int(int(root.find("./playback/duration").text)) / 1000

46
shapes.py Normal file
View file

@ -0,0 +1,46 @@
from dataclasses import dataclass
from pathlib import Path
from typing import List
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from download import fetch_file
@dataclass
class Slide:
id: str
start: float
end: float
filename: str
width: float
height: float
@property
def file(self):
return fetch_file(self.filename)
class Shapes:
def __init__(self, xml_file: Path):
tree = ElementTree.parse(xml_file)
root = tree.getroot()
image: Element
self.slides: List[Slide] = []
self.maxwidth = 0
self.maxheight = 0
for image in root:
data = image.attrib
slide = Slide(
id=data["id"],
start=float(data["in"]),
end=float(data["out"]),
filename=data["{http://www.w3.org/1999/xlink}href"],
width=int(data["width"]),
height=int(data["height"]),
)
if slide.width > self.maxwidth:
self.maxwidth = slide.width
if slide.height > self.maxheight:
self.maxheight = slide.height
self.slides.append(slide)