diff --git a/README.md b/README.md index fbb87fb..a51a187 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The `ffmpeg4discord.py` script takes a video file as its input and encodes it to The `TwoPass()` Class showcases a 2-pass encoding methodology for the `ffmpeg-python` library, which is not well-documented on the web. The Class also supports a few ffmpeg video filters, like cropping and resolution scaling. It can be used in a variety of different audio/video workflows. -## Usage +## Installation and Usage You must first have `ffmpeg` installed on your system. `ffmpeg` needs to be registered in your PATH. Install the required Python packages, which includes `ffmpeg-python`, with: @@ -18,7 +18,7 @@ Call the script with: The included Batch file for Windows users, `encode.bat`, allows for drag and drop functionality. Be sure to edit the Batch file before dragging your video files on top of it. -### Special install instructions for Windows users +### Special instructions for Windows users If you do not have ffmpeg installed, you can use the included `windows_setup.py` file to do about 90% of the installation. @@ -50,6 +50,8 @@ This script downloads ffmpeg, extracts it into the current directory, and launch - Example: `255x0x1410x1080` - From the top-left of your video, this example goes 255 pixels to the right, 0 pixels down, and it carves out a 1410x1080 section of the video. - [ffmpeg crop documentation](https://ffmpeg.org/ffmpeg-filters.html#Examples-61) +- `--web` + - A Boolean flag. No value is needed after the flag. See [Web UI](#web-ui) for more information on the Web UI. - `--config` - Example: `custom_run_config.json` - Path to a json file containing the configuration for the above parameters. This config file takes precedence over all of the other flags. @@ -106,3 +108,25 @@ python D:/ffmpeg4discord/ffmpeg4discord.py 000050-000145.mp4 \ The example above takes a 5120x1440 resolution video as its input. The script trims the video from 00:00:50 to 00:01:45 (specified in the [file name](https://github.com/zfleeman/ffmpeg4discord#file-name-formatting)). It crops a 2560x1440 section starting at 1280 pixels from the top-left and 0 pixels down (`-c`). The output file will be located in `D:/shadowplay/` (`-o`) with a new resolution of 1920x1080 (`-r`), and it will be 50MB (`-s`). The audio bitrate will be reduced to 48k (`-a`) as well, but that's probably going to sound terrible. ![](https://i.imgur.com/WJXA723.png) + +## Web UI + +The Web UI can be activated by adding `--web` to your `ffmpeg4discord.py` call. + +``` +python "C:/path/to/ffmpeg4discord.py" cool_clip.mp4 -r 1280x720 -s 20 --web +``` + +That command will spin up a Flask server on your local machine and launch a rendered webpage with the video as the centerpiece. The flags you provide to the `python` statement will fill in the defaults for the form. You can override/replace the values. + +You can drag the video playhead to different portions of the video and click the "Set Start/End Time" buttons to specify the section of the video you want to be clipped out. You can also use the range sliders underneath the buttons if you prefer. A "Preview Selection" button is provided for your convenience, and it does what it sounds like. + +https://github.com/zfleeman/ffmpeg4discord/assets/1808564/ff323bcb-4747-437b-808f-ce48b8c269ce + +The Flask server doesn't automatically stop itself, yet, so you'll have to handle that by closing the terminal it leaves hanging. + +## Thanks! + +Yes, this is a simple collection of Python files using FFmpeg tricks that is masquerading as a robust Audio/Video tool. But! I use this nearly every day to quickly share videos with people on various messaging apps that have built-in video players. I don't have to share a link that embeds a video player this way, and I guess that's important to me? + +I like working on this! Enjoy! diff --git a/ffmpeg4discord.py b/ffmpeg4discord.py index 3fc594f..42b29c4 100644 --- a/ffmpeg4discord.py +++ b/ffmpeg4discord.py @@ -1,21 +1,98 @@ +import sys import os +from glob import glob +import webbrowser +from flask import Flask, render_template, url_for, request +from random import randint +import time +import threading +from pathlib import Path + +sys.dont_write_bytecode = True from utils.arguments import get_args from twopass import TwoPass + # get args from the command line args = get_args() +web = args.pop("web") +path = Path(args["filename"]).resolve() +args["filename"] = path -# instantiate the TwoPass class and save our target file size for comparison in the loop +# instantiate the TwoPass class twopass = TwoPass(**args) -end_fs = args["target_filesize"] -while twopass.run() >= end_fs: + +def twopass_loop(target_filesize: float): + while twopass.run() >= target_filesize: + print( + f"\nThe output file size ({round(twopass.output_filesize, 2)}MB) is still above the target of {target_filesize}MB.\nRestarting...\n" + ) + os.remove(twopass.output_filename) + + # adjust the class's target file size to set a lower bitrate for the next run + twopass.target_filesize -= 0.2 + print( - f"\nThe output file size ({round(twopass.output_filesize, 2)}MB) is still above the target of {end_fs}MB.\nRestarting...\n" + f"\nSUCCESS!!\nThe smaller file ({round(twopass.output_filesize, 2)}MB) is located at {twopass.output_filename}" ) - os.remove(twopass.output_filename) - # adjust the class's target file size to set a lower bitrate for the next run - twopass.target_filesize -= 0.2 -print(f"\nSUCCESS!!\nThe smaller file ({round(twopass.output_filesize, 2)}MB) is located at {twopass.output_filename}") +def seconds_to_timestamp(seconds: int): + hours, remainder = divmod(seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + # Use f-strings to format the timestamp + timestamp = f"{hours:02d}:{minutes:02d}:{seconds:02d}" + + return timestamp + + +def open_browser(): + time.sleep(0.5) + webbrowser.open(f"http://localhost:{port}") + + +if web: + app = Flask(__name__, static_folder=path.parent) + + @app.route("/") + def index(): + return render_template( + "web.html", + filename=url_for("static", filename=path.name), + resolution=twopass.resolution, + target_filesize=twopass.target_filesize, + audio_br=twopass.audio_br, + crop=twopass.crop, + output_dir=twopass.output_dir, + ) + + @app.route("/encode", methods=["POST"]) + def form_twopass(): + # generate new times from the selection + ss = int(request.form.get("startTime")) + to = int(request.form.get("endTime")) + twopass.length = to - ss + twopass.times = {"ss": seconds_to_timestamp(ss), "to": seconds_to_timestamp(to)} + target_filesize = float(request.form.get("target_filesize")) + + # update TwoPass from web form + twopass.resolution = request.form.get("resolution") + twopass.target_filesize = target_filesize + twopass.audio_br = float(request.form.get("audio_br")) * 1000 + twopass.crop = request.form.get("crop") + twopass.output_dir = request.form.get("output_dir") + + twopass_loop(target_filesize) + + for file in glob("ffmpeg2pass*"): + os.remove(file) + + return f"Your compressed video file ({round(twopass.output_filesize, 2)}MB) is located at {Path(twopass.output_filename).resolve()}" + + port = randint(5000, 6000) + threading.Thread(target=open_browser, name="Open Browser").start() + app.run("0.0.0.0", port=port) +else: + twopass_loop(args["target_filesize"]) diff --git a/requirements.txt b/requirements.txt index 2b17fe3..569c442 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ ffmpeg-python pyperclip +flask diff --git a/templates/web.html b/templates/web.html new file mode 100644 index 0000000..c76874b --- /dev/null +++ b/templates/web.html @@ -0,0 +1,144 @@ + + + + + + The Ephemeral Web UI for ffmpeg4discord + + + + +
+
+ +

Drag the video playhead, and use the buttons below to set the Start and End times.

+
+
+
+
+ +
+
+ +
+
+
+
+ + + 0 +
+
+ + + 0 +
+
+
+
+ +
+
+
+
+

Output Parameters

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + + diff --git a/twopass/twopass.py b/twopass/twopass.py index 3d26cf6..2708a6a 100644 --- a/twopass/twopass.py +++ b/twopass/twopass.py @@ -4,6 +4,7 @@ import json import os from datetime import datetime +from pathlib import Path logging.getLogger().setLevel(logging.INFO) @@ -11,7 +12,7 @@ class TwoPass: def __init__( self, - filename: str, + filename: Path, target_filesize: float, output_dir: str = "", times: dict = {}, @@ -49,7 +50,7 @@ def __init__( self.codec = codec self.filename = filename - self.fname = self.filename.replace("\\", "/").split("/")[-1] + self.fname = filename.name self.split_fname = self.fname.split(".") self.output_dir = output_dir @@ -74,13 +75,6 @@ def __init__( else: self.audio_br = self.audio_br * 1000 - self.output_filename = ( - self.output_dir - + "small_" - + self.split_fname[0].replace(" ", "_") - + datetime.strftime(datetime.now(), "_%Y%m%d%H%M%S.mp4") - ) - if self.times: if self.times.get("from"): self.times["ss"] = self.times["from"] or "00:00:00" @@ -217,6 +211,14 @@ def run(self) -> float: Perform the CPU-intensive encoding job :return: the output file's size """ + + self.output_filename = ( + self.output_dir + + "small_" + + self.filename.stem.replace(" ", "_") + + datetime.strftime(datetime.now(), "_%Y%m%d%H%M%S.mp4") + ) + # generate run parameters self.create_bitrate_dict() params = self.generate_params(codec=self.codec) diff --git a/utils/arguments.py b/utils/arguments.py index 67709c9..8b4337c 100644 --- a/utils/arguments.py +++ b/utils/arguments.py @@ -1,4 +1,4 @@ -from argparse import ArgumentParser, Namespace +from argparse import ArgumentParser, Namespace, BooleanOptionalAction def get_args() -> Namespace: @@ -7,7 +7,7 @@ def get_args() -> Namespace: description="Video compression script.", epilog="Compress those sick clips, boi.", ) - + parser.add_argument("--web", action=BooleanOptionalAction) parser.add_argument("filename", help="The full file path of the file that you wish to compress.") parser.add_argument( "-o", @@ -22,11 +22,11 @@ def get_args() -> Namespace: type=float, help="The output file size in MB. Free Discord accepts a max of 8MB.", ) - parser.add_argument("-a", "--audio-br", type=float, help="Audio bitrate in kbps.") + parser.add_argument("-a", "--audio-br", type=float, default=96, help="Audio bitrate in kbps.") # video filters - parser.add_argument("-x", "--crop", help="Cropping dimensions. Example: 255x0x1410x1080") - parser.add_argument("-r", "--resolution", help="The output resolution of your final video.") + parser.add_argument("-x", "--crop", default="", help="Cropping dimensions. Example: 255x0x1410x1080") + parser.add_argument("-r", "--resolution", default="", help="The output resolution of your final video.") # configuraiton json file parser.add_argument("--config", help="JSON file containing the run's configuration")