Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two Pass Encoding Class #10

Merged
merged 8 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ RUN apk add --no-cache ffmpeg
RUN pip install ffmpeg-python
RUN mkdir -p /usr/app/out/
WORKDIR /usr/app/
COPY discord.py .
ENTRYPOINT ["python", "-u", "discord.py", "-o", "/usr/app/out/"]
COPY ffmpeg4discord.py .
ENTRYPOINT ["python", "-u", "ffmpeg4discord.py", "-o", "/usr/app/out/"]
6 changes: 6 additions & 0 deletions conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"target_filesize": 8.0,
"audio_br": 96,
"crop": "",
"resolution": ""
}
184 changes: 0 additions & 184 deletions discord.py

This file was deleted.

3 changes: 2 additions & 1 deletion encode.bat
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@echo off
Set filename=%1
python "C:/path/to/discord.py" %filename% -o "C:/output/folder/"
python "C:/path/to/ffmpeg4discord.py" %filename% -o "C:/output/folder/"
DEL "ffmpeg2*"
PAUSE
23 changes: 23 additions & 0 deletions ffmpeg4discord.py
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
from utils.arguments import get_args
from twopass import TwoPass

args = get_args()
twopass = TwoPass(**args)
end_fs = args["target_filesize"]

run = True

while run:
twopass.run()

output_fs = os.path.getsize(twopass.output_filename) * 0.00000095367432
run = end_fs <= output_fs
output_fs = round(output_fs, 2)

if run:
print(f"Output file size ({output_fs}MB) still above the target of {end_fs}MB.\nRestarting...\n")
os.remove(twopass.output_filename)
twopass.target_filesize -= 0.2
else:
print(f"\nSUCCESS!!\nThe smaller file ({output_fs}MB) is located at {twopass.output_filename}")
1 change: 1 addition & 0 deletions twopass/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .twopass import TwoPass
155 changes: 155 additions & 0 deletions twopass/twopass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import ffmpeg
import math
import logging
import json
from datetime import datetime

logging.getLogger().setLevel(logging.INFO)


class TwoPass:
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
def __init__(
self,
filename: str,
output_dir: str,
target_filesize: float,
audio_br: float = 96,
codec: str = "libx264",
crop: str = "",
resolution: str = "",
config_file: str = "",
) -> None:
self.codec = codec
self.filename = filename
self.file_info = ffmpeg.probe(filename=self.filename)
self.output_dir = output_dir

if config_file:
self.init_from_config(config_file=config_file)
else:
self.target_filesize = target_filesize
self.audio_br = audio_br
self.crop = crop
self.resolution = resolution

self.fname = self.filename.replace("\\", "/").split("/")[-1]
self.split_fname = self.fname.split(".")

self.output_filename = (
self.output_dir
+ "small_"
+ self.split_fname[0].replace(" ", "_")
+ datetime.strftime(datetime.now(), "_%Y%m%d%H%M%S.mp4")
)

self.input_ratio = self.file_info["streams"][0]["width"] / self.file_info["streams"][0]["height"]
self.duration = math.floor(float(self.file_info["format"]["duration"]))
self.time_calculations()

def init_from_config(self, config_file: str) -> None:
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
with open(config_file) as f:
config = json.load(f)
self.__dict__.update(**config)

def generate_params(self, codec: str):
params = {
"pass1": {
"pass": 1,
"f": "null",
"vsync": "cfr", # not sure if this is unique to x264 or not
"c:v": codec,
},
"pass2": {"pass": 2, "b:a": self.audio_br * 1000, "c:v": codec},
}

if codec == "libx264":
params["pass2"]["c:a"] = "aac"
elif codec == "vp9":
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
# still a lot of work here
params["pass2"]["c:a"] = "libopus"

params["pass1"].update(**self.bitrate_dict)
params["pass2"].update(**self.bitrate_dict)

return params

def create_bitrate_dict(self) -> None:
br = math.floor((self.target_filesize * 8192) / self.length - self.audio_br) * 1000
bitrate_dict = {
"b:v": br,
"minrate": br * 0.5,
"maxrate": br * 1.45,
"bufsize": br * 2,
}
self.bitrate_dict = bitrate_dict
zfleeman marked this conversation as resolved.
Show resolved Hide resolved

def time_calculations(self):
zfleeman marked this conversation as resolved.
Show resolved Hide resolved
fname = self.fname
startstring = fname[0:2] + ":" + fname[2:4] + ":" + fname[4:6]
endstring = fname[7:9] + ":" + fname[9:11] + ":" + fname[11:13]
times = {}

try:
int(fname[0:6])
startseconds = int(fname[0:2]) * 60 * 60 + int(fname[2:4]) * 60 + int(fname[4:6])
times["ss"] = startstring
try:
int(fname[11:13])
endseconds = int(fname[7:9]) * 60 * 60 + int(fname[9:11]) * 60 + int(fname[11:13])
length = endseconds - startseconds
times["to"] = endstring
except:
length = self.duration - startseconds
except:
length = self.duration

if length <= 0:
raise Exception(
f"Your video is {self.duration / 60} minutes long, but you wanted to start clpping at {self.times['ss']}"
)

self.length = length
self.times = times

def apply_video_filters(self, ffinput):
video = ffinput.video

if self.crop:
crop = self.crop.split("x")
video = video.crop(x=crop[0], y=crop[1], width=crop[2], height=crop[3])
self.inputratio = int(crop[2]) / int(crop[3])

if self.resolution:
video = video.filter("scale", self.resolution)
x = int(self.resolution.split("x")[0])
y = int(self.resolution.split("x")[1])
outputratio = x / y

if self.inputratio != outputratio:
logging.warning(
"Your output resolution's aspect ratio does not match the\ninput resolution's or your croped resolution's aspect ratio."
)

return video

def run(self):
# generate run parameters
self.create_bitrate_dict()
params = self.generate_params(codec=self.codec)

# separate streams from ffinput
ffinput = ffmpeg.input(self.filename, **self.times)
video = self.apply_video_filters(ffinput)
audio = ffinput.audio

# First Pass
ffOutput = ffmpeg.output(video, "pipe:", **params["pass1"])
ffOutput = ffOutput.global_args("-loglevel", "quiet", "-stats")
print("Performing first pass")
std_out, std_err = ffOutput.run(capture_stdout=True)

# Second Pass
ffOutput = ffmpeg.output(video, audio, self.output_filename, **params["pass2"])
ffOutput = ffOutput.global_args("-loglevel", "quiet", "-stats")
print("\nPerforming second pass")
ffOutput.run(overwrite_output=True)
Loading
Loading