-
-
Save ESWZY/a420a308d3118f21274a0bc3a6feb1ff to your computer and use it in GitHub Desktop.
# Simplified version and explanation at: https://stackoverflow.com/a/64439347/12866353 | |
import os | |
import ffmpeg | |
def compress_video(video_full_path, size_upper_bound, two_pass=True, filename_suffix='cps_'): | |
""" | |
Compress video file to max-supported size. | |
:param video_full_path: the video you want to compress. | |
:param size_upper_bound: Max video size in KB. | |
:param two_pass: Set to True to enable two-pass calculation. | |
:param filename_suffix: Add a suffix for new video. | |
:return: out_put_name or error | |
""" | |
filename, extension = os.path.splitext(video_full_path) | |
extension = '.mp4' | |
output_file_name = filename + filename_suffix + extension | |
# Adjust them to meet your minimum requirements (in bps), or maybe this function will refuse your video! | |
total_bitrate_lower_bound = 11000 | |
min_audio_bitrate = 32000 | |
max_audio_bitrate = 256000 | |
min_video_bitrate = 100000 | |
try: | |
# Bitrate reference: https://en.wikipedia.org/wiki/Bit_rate#Encoding_bit_rate | |
probe = ffmpeg.probe(video_full_path) | |
# Video duration, in s. | |
duration = float(probe['format']['duration']) | |
# Audio bitrate, in bps. | |
audio_bitrate = float(next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)['bit_rate']) | |
# Target total bitrate, in bps. | |
target_total_bitrate = (size_upper_bound * 1024 * 8) / (1.073741824 * duration) | |
if target_total_bitrate < total_bitrate_lower_bound: | |
print('Bitrate is extremely low! Stop compress!') | |
return False | |
# Best min size, in kB. | |
best_min_size = (min_audio_bitrate + min_video_bitrate) * (1.073741824 * duration) / (8 * 1024) | |
if size_upper_bound < best_min_size: | |
print('Quality not good! Recommended minimum size:', '{:,}'.format(int(best_min_size)), 'KB.') | |
# return False | |
# Target audio bitrate, in bps. | |
audio_bitrate = audio_bitrate | |
# target audio bitrate, in bps | |
if 10 * audio_bitrate > target_total_bitrate: | |
audio_bitrate = target_total_bitrate / 10 | |
if audio_bitrate < min_audio_bitrate < target_total_bitrate: | |
audio_bitrate = min_audio_bitrate | |
elif audio_bitrate > max_audio_bitrate: | |
audio_bitrate = max_audio_bitrate | |
# Target video bitrate, in bps. | |
video_bitrate = target_total_bitrate - audio_bitrate | |
if video_bitrate < 1000: | |
print('Bitrate {} is extremely low! Stop compress.'.format(video_bitrate)) | |
return False | |
i = ffmpeg.input(video_full_path) | |
if two_pass: | |
ffmpeg.output(i, os.devnull, | |
**{'c:v': 'libx264', 'b:v': video_bitrate, 'pass': 1, 'f': 'mp4'} | |
).overwrite_output().run() | |
ffmpeg.output(i, output_file_name, | |
**{'c:v': 'libx264', 'b:v': video_bitrate, 'pass': 2, 'c:a': 'aac', 'b:a': audio_bitrate} | |
).overwrite_output().run() | |
else: | |
ffmpeg.output(i, output_file_name, | |
**{'c:v': 'libx264', 'b:v': video_bitrate, 'c:a': 'aac', 'b:a': audio_bitrate} | |
).overwrite_output().run() | |
if os.path.getsize(output_file_name) <= size_upper_bound * 1024: | |
return output_file_name | |
elif os.path.getsize(output_file_name) < os.path.getsize(video_full_path): # Do it again | |
return compress_video(output_file_name, size_upper_bound) | |
else: | |
return False | |
except FileNotFoundError as e: | |
print('You do not have ffmpeg installed!', e) | |
print('You can install ffmpeg by reading https://github.com/kkroening/ffmpeg-python/issues/251') | |
return False | |
if __name__ == '__main__': | |
file_name = compress_video('input.mp4', 50 * 1000) | |
print(file_name) |
@ribrea Oh, this is just a typo. I mean compress_video()
.
Fixed. Thank you!
why is import error coming for ffmpeg
why is import error coming for ffmpeg
Maybe you don't have ffmpeg installed. Please refer to this page.
A strongly typed, more vivid version of the original one
I did some typing and a little more readable. I was tends to be a JS developer, a picky one and later I fall in love with Typescript, that's why I I like to make things strongly typed. And last change was making function blind about the output and where it should saves the output, In my use case I wanted this function as a utility function to be useable in every platform. I mean in django I will save files in another place and in pure python in another place.
from typing import Any
from typing import NoReturn
import ffmpeg
import os
def resize_video(
video_absolute_path: str,
output_file_absolute_path: str,
size_upper_bound: int,
two_pass: bool=True,) -> str:
"""
Compress video file to max-supported size.
:param video_absolute_path: the video you want to compress.
:param size_upper_bound: Max video size in KB.
:param two_pass: Set to True to enable two-pass calculation.
:param filename_suffix: Add a suffix for new video.
:return: out_put_name or error
"""
# Bitrate reference: https://en.wikipedia.org/wiki/Bit_rate#Encoding_bit_rate
probe_json_representation = ffmpeg.probe(video_absolute_path)
# Video duration, in s.
duration = float(probe_json_representation['format']['duration'])
# Audio bitrate, in bps.
streams: list[dict] = probe_json_representation['streams']
# {'index': 0, 'codec_name': 'h264', 'codec_long_name': 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10', 'profile': 'Main', 'codec_type': 'video', 'codec_tag_string': 'avc1', 'codec_tag': '0x31637661', 'width': 1280, 'height': 720, 'coded_width': 1280, 'coded_height': 720, 'closed_captions': 0, 'film_grain': 0, 'has_b_frames': 1, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': 31, 'color_range': 'tv', 'color_space': 'bt709', 'color_transfer': 'bt709', 'color_primaries': 'bt709', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'is_avc': 'true', 'nal_length_size': '4', 'id': '0x1', 'r_frame_rate': '25/1', 'avg_frame_rate': '25/1', 'time_base': '1/12800', 'start_pts': 0, 'start_time': '0.000000', 'duration_ts': 4096000, 'duration': '320.000000', 'bit_rate': '71355', 'bits_per_raw_sample': '8', 'nb_frames': '8000', 'extradata_size': 43, 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'creation_time': '2022-06-14T17:31:48.000000Z', 'language': 'und', 'handler_name': 'ISO Media file produced by Google Inc. Created on: 06/14/2022.', 'vendor_id': '[0][0][0][0]'}}
stream: dict[str, Any]|None = next(
(stream for stream in streams if stream['codec_type'] == 'audio'),
None,
)
assert stream is not None,\
"Stream is None, streams had not include any item with audio codec_type"
# e.x. '654874'
bit_rate: str = stream['bit_rate']
audio_bitrate = float(bit_rate)
# Target total bitrate, in bps.
target_total_bitrate = (size_upper_bound * 1024 * 8) / (1.073741824 * duration)
min_audio_bitrate = 32000
# Target video bitrate, in bps.
video_bitrate = target_total_bitrate - audio_bitrate
# FIXME: IDK why but for some reason this function raise exception in
# any case. BTW I comment it just for now
# check_bitrate(
# duration,
# size_upper_bound,
# target_total_bitrate,
# min_audio_bitrate,
# video_bitrate,
# )
# target audio bitrate, in bps
max_audio_bitrate = 256000
if 10 * audio_bitrate > target_total_bitrate:
audio_bitrate = target_total_bitrate / 10
if audio_bitrate < min_audio_bitrate < target_total_bitrate:
audio_bitrate = min_audio_bitrate
elif audio_bitrate > max_audio_bitrate:
audio_bitrate = max_audio_bitrate
i = ffmpeg.input(video_absolute_path)
if two_pass:
ffmpeg.output(
i,
'/dev/null' if os.path.exists('/dev/null') else 'NUL',
**{'c:v': 'libx264', 'b:v': video_bitrate, 'pass': 1, 'f': 'mp4'}
).overwrite_output().run()
ffmpeg.output(
i,
output_file_absolute_path,
**{'c:v': 'libx264', 'b:v': video_bitrate, 'pass': 2, 'c:a': 'aac', 'b:a': audio_bitrate}
).overwrite_output().run()
else:
ffmpeg.output(
i,
output_file_absolute_path,
**{'c:v': 'libx264', 'b:v': video_bitrate, 'c:a': 'aac', 'b:a': audio_bitrate}
).overwrite_output().run()
if os.path.getsize(output_file_absolute_path) <= size_upper_bound * 1024:
return output_file_absolute_path
elif os.path.getsize(output_file_absolute_path) < os.path.getsize(video_absolute_path): # Do it again
return resize_video(
video_absolute_path=output_file_absolute_path,
output_file_absolute_path=output_file_absolute_path,
size_upper_bound=size_upper_bound
)
else:
raise Exception('Resize failed')
def check_bitrate(
duration: float,
size_upper_bound: int,
target_total_bitrate: float,
min_audio_bitrate: int,
video_bitrate: float,) -> None|NoReturn:
total_bitrate_lower_bound = 11000
min_video_bitrate = 100000
assert target_total_bitrate < total_bitrate_lower_bound, \
'Bitrate is extremely low! Stop compress!'
# Best min size, in kB.
best_min_size = (min_audio_bitrate + min_video_bitrate) * (1.073741824 * duration) / (8 * 1024)
assert size_upper_bound < best_min_size, \
f"Quality not good! Recommended minimum size: {int(best_min_size)} KB."
assert video_bitrate < 1000, \
f'Bitrate ({video_bitrate}) is extremely low! Stop compress.'
# This example turned 120 MB into 34 MB
# Note that this is a really CPU intensive process. The Anime is around 24 minute. I guess using processes is wiser than doing it in normal way
file_absolute_path = resize_video(
video_absolute_path='/home/kasir/Videos/[12] The Rising of the Shield Hero Season 2.mp4',
size_upper_bound=50 * 1000,
output_file_absolute_path="/tmp/media/some-name.mp4",
)
print(file_absolute_path)
Question
@ESWZY Do you have any idea about check_bitrate
function? it just raise exception in any case no matter what I pass to it
@kasir-barati Hi Kasir, well done!
The reason why I use these bit rate checks is that low bit rate video is not suitable for my scenario. And if you encounter some problems, you can just comment them out, or adjust following constants to meet your requirements. I will also add some comments to point out that.
total_bitrate_lower_bound = 11000
min_audio_bitrate = 32000
max_audio_bitrate = 256000
min_video_bitrate = 100000
I just remembered, don't delete video_bitrate < 1000
checking step, or FFmpeg will raise an exception.
Hi. I'm trying to use these codes, but all three versions keep giving me errors:
-The StackOverflow version runs up to the second ffmpeg.output where it terminates with a [NULL @ memory address] Unable to find a suitable output format for 'input path' 'output path': invalid argument.
-The full version doesn't recognize the key 'format' in duration = float(probe['format']['duration']).
-The typed version doesn't recognize the type -> None|NoReturn at the check_bitrate function.
Do you have any idea of a possible solution?
Hi. I'm trying to use these codes, but all three versions keep giving me errors:
-The StackOverflow version runs up to the second ffmpeg.output where it terminates with a [NULL @ memory address] Unable to find a suitable output format for 'input path' 'output path': invalid argument.
-The full version doesn't recognize the key 'format' in duration = float(probe['format']['duration']).
-The typed version doesn't recognize the type -> None|NoReturn at the check_bitrate function.
Do you have any idea of a possible solution?
It looks like some unsupported file type. Could you give us some details about your file?
Hi. I'm trying to use these codes, but all three versions keep giving me errors:
-The StackOverflow version runs up to the second ffmpeg.output where it terminates with a [NULL @ memory address] Unable to find a suitable output format for 'input path' 'output path': invalid argument.
-The full version doesn't recognize the key 'format' in duration = float(probe['format']['duration']).
-The typed version doesn't recognize the type -> None|NoReturn at the check_bitrate function.
Do you have any idea of a possible solution?It looks like some unsupported file type. Could you give us some details about your file?
It's just an mp4 file. If you need any other info, you might have to point me to how I can get it.
I tried the following code and it worked, but your solution seems more flexible, so I'd still like to make it work.
import os
import subprocess
import ffmpeg
subprocess.run('ffmpeg -i input.mp4 -vcodec h264 -acodec mp2 output.mp4')
Hi. I'm trying to use these codes, but all three versions keep giving me errors:
-The StackOverflow version runs up to the second ffmpeg.output where it terminates with a [NULL @ memory address] Unable to find a suitable output format for 'input path' 'output path': invalid argument.
-The full version doesn't recognize the key 'format' in duration = float(probe['format']['duration']).
-The typed version doesn't recognize the type -> None|NoReturn at the check_bitrate function.
Do you have any idea of a possible solution?It looks like some unsupported file type. Could you give us some details about your file?
It's just an mp4 file. If you need any other info, you might have to point me to how I can get it.
I tried the following code and it worked, but your solution seems more flexible, so I'd still like to make it work.
import os import subprocess import ffmpeg subprocess.run('ffmpeg -i input.mp4 -vcodec h264 -acodec mp2 output.mp4')
I think it's a version mismatch or a file format problem. I have not tested this code in many platforms. If you know the duration of your video, you can assign a value directly to variable duration.
I am getting this error with the first code example and the simplified version on stackoverflow. Any solution?
audio_bitrate = float(next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)['bit_rate'])
TypeError: 'NoneType' object is not subscriptable
I am getting this error with the first code example and the simplified version on stackoverflow. Any solution?
audio_bitrate = float(next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)['bit_rate'])
TypeError: 'NoneType' object is not subscriptable
Please print out the probe
variable. Maybe something is missing here.
Spoiler - fixed it allready
{'streams': [{'index': 0, 'codec_name': 'h264', 'codec_long_name': 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10', 'profile': 'High', 'codec_type': 'video', 'codec_time_base': '1001/60000', 'codec_tag_string': 'avc1', 'codec_tag': '0x31637661', 'width': 1920, 'height': 1080, 'coded_width': 1920, 'coded_height': 1088, 'closed_captions': 0, 'has_b_frames': 1, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': 40, 'color_range': 'tv', 'color_space': 'bt709', 'color_transfer': 'bt709', 'color_primaries': 'bt709', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'is_avc': 'true', 'nal_length_size': '4', 'r_frame_rate': '30000/1001', 'avg_frame_rate': '30000/1001', 'time_base': '1/30000', 'start_pts': 0, 'start_time': '0.000000', 'duration_ts': 23635612, 'duration': '787.853733', 'bit_rate': '18752', 'bits_per_raw_sample': '8', 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0}, 'tags': {'creation_time': '2021-04-03T13:57:24.000000Z', 'language': 'und', 'handler_name': 'ISO Media file produced by Google Inc.'}}], 'format': {'filename': 'input.mp4', 'nb_streams': 1, 'nb_programs': 0, 'format_name': 'mov,mp4,m4a,3gp,3g2,mj2', 'format_long_name': 'QuickTime / MOV', 'start_time': '0.000000', 'duration': '787.853733', 'size': '196029230', 'bit_rate': '1990513', 'probe_score': 100, 'tags': {'major_brand': 'dash', 'minor_version': '0', 'compatible_brands': 'iso6avc1mp41', 'creation_time': '2021-04-03T13:57:24.000000Z'}}}my ytdl params are ydl_opts = {'cookiefile': 'cookies.txt',
'output': 'C:/xxx/videos',
'outtmpl': 'input.mp4',
'format': 'bestvideo[ext=mp4]',
'progress_hooks': [self.my_hook]}
My format was without audio, so i have to add
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio',
Next problem:
Spoiler - fixed it allready too
Now a new problem occures:raise Error('ffprobe', out, err)
ffmpeg._run.Error: ffprobe error (see stderr output for detail). i found out that there is a missing av1 encoder, but not how to solve the problem.
Fixed it allready by updating ffmpeg.
Now this error occures:
duration = float(probe['format']['duration'])
KeyError: 'format'
Spoiler - fixed it allready
{'streams': [{'index': 0, 'codec_name': 'h264', 'codec_long_name': 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10', 'profile': 'High', 'codec_type': 'video', 'codec_time_base': '1001/60000', 'codec_tag_string': 'avc1', 'codec_tag': '0x31637661', 'width': 1920, 'height': 1080, 'coded_width': 1920, 'coded_height': 1088, 'closed_captions': 0, 'has_b_frames': 1, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': 40, 'color_range': 'tv', 'color_space': 'bt709', 'color_transfer': 'bt709', 'color_primaries': 'bt709', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'is_avc': 'true', 'nal_length_size': '4', 'r_frame_rate': '30000/1001', 'avg_frame_rate': '30000/1001', 'time_base': '1/30000', 'start_pts': 0, 'start_time': '0.000000', 'duration_ts': 23635612, 'duration': '787.853733', 'bit_rate': '18752', 'bits_per_raw_sample': '8', 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0}, 'tags': {'creation_time': '2021-04-03T13:57:24.000000Z', 'language': 'und', 'handler_name': 'ISO Media file produced by Google Inc.'}}], 'format': {'filename': 'input.mp4', 'nb_streams': 1, 'nb_programs': 0, 'format_name': 'mov,mp4,m4a,3gp,3g2,mj2', 'format_long_name': 'QuickTime / MOV', 'start_time': '0.000000', 'duration': '787.853733', 'size': '196029230', 'bit_rate': '1990513', 'probe_score': 100, 'tags': {'major_brand': 'dash', 'minor_version': '0', 'compatible_brands': 'iso6avc1mp41', 'creation_time': '2021-04-03T13:57:24.000000Z'}}}
my ytdl params are ydl_opts = {'cookiefile': 'cookies.txt', 'output': 'C:/xxx/videos', 'outtmpl': 'input.mp4', 'format': 'bestvideo[ext=mp4]', 'progress_hooks': [self.my_hook]}My format was without audio, so i have to add
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio',
Next problem:
Spoiler - fixed it allready too
Now a new problem occures:raise Error('ffprobe', out, err)
ffmpeg._run.Error: ffprobe error (see stderr output for detail). i found out that there is a missing av1 encoder, but not how to solve the problem.
Fixed it allready by updating ffmpeg.
Now this error occures:
duration = float(probe['format']['duration'])
KeyError: 'format'
Thanks for your report! I have not tested videos without audio, and I will fix it.
It's weird that your probe
already has this field format
, but reports missing here. Please print it again to have a look.🧐
Thats all really strange. the print of probe is empty.
that´s because ytdl does not merge audio and video before the hook starts.
This is my code:
def compress_video(self, video_full_path, output_file_name, target_size):
# Reference: https://en.wikipedia.org/wiki/Bit_rate#Encoding_bit_rate
min_audio_bitrate = 32000
max_audio_bitrate = 256000
probe = ffmpeg.probe(video_full_path)
print("Probe")
print(probe)
# Video duration, in s.
duration = float(probe['format']['duration'])
# Audio bitrate, in bps.
audio_bitrate = float(next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)['bit_rate'])
# Target total bitrate, in bps.
target_total_bitrate = (target_size * 1024 * 8) / (1.073741824 * duration)
# Target audio bitrate, in bps
if 10 * audio_bitrate > target_total_bitrate:
audio_bitrate = target_total_bitrate / 10
if audio_bitrate < min_audio_bitrate < target_total_bitrate:
audio_bitrate = min_audio_bitrate
elif audio_bitrate > max_audio_bitrate:
audio_bitrate = max_audio_bitrate
# Target video bitrate, in bps.
video_bitrate = target_total_bitrate - audio_bitrate
i = ffmpeg.input(video_full_path)
ffmpeg.output(i, os.devnull,
**{'c:v': 'libx264', 'b:v': video_bitrate, 'pass': 1, 'f': 'mp4'}
).overwrite_output().run()
ffmpeg.output(i, output_file_name,
**{'c:v': 'libx264', 'b:v': video_bitrate, 'pass': 2, 'c:a': 'aac', 'b:a': audio_bitrate}
).overwrite_output().run()
def my_hook(self, d):
if d['status'] == 'finished':
filename = d['filename']
print(filename)
self.compress_video(filename, 'output.mp4', 50 * 1000)
print('Done downloading, now converting ...')
async def download(self):
ydl_opts = {'cookiefile': 'cookies.txt',
'output': 'C:/xxx/videos',
'outtmpl': 'input.mp4',
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
'progress_hooks': [self.my_hook]}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download('https://www.youtube.com/watch?v=CXkkUOCfnOQ')
with this i get
audio_bitrate = float(next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)['bit_rate'])
TypeError: 'NoneType' object is not subscriptable
because there is a input.f137.mp4 file with no audio only.
ytdl does not merge my files before i convert with ffmpeg. really strange
@Inge1234567890 Yes, because the d['filename']
is just the video part, not the full part.
Maybe you can ues meta = ydl.extract_info(url, download=True)
instead. And read the meta['ext']
(or meta['entries'][0]['ext']
) to get the full filename.
@ESWZY hi I used your code and it works perfectly for my project. I am new to python so I struggling on how to get the stdout to get the realtime status to a progressbar.can you help me out?
@ESWZY hi I used your code and it works perfectly for my project. I am new to python so I struggling on how to get the stdout to get the realtime status to a progressbar.can you help me out?
That's interesting! But I think it is difficult to get the stdout directly from the code above without modifications.
As an idea, the ffmpeg library in Python
just calls the ffmpeg binary
, you can use this library as an alternative (just rename this library as ffmpeg
):
https://github.com/althonos/ffpb
Or, you can parse the output of this Python snippet by following answers:
https://stackoverflow.com/questions/747982/can-ffmpeg-show-a-progress-bar
Do you have any function to compress a video file in Django in production? Thanks
skvideo.io.FFmpegWriter con esta Clase es posible utiliar H264, una fiesta
Dear @ESWZY
where is
save_compressed_video()
?