Skip to content

Instantly share code, notes, and snippets.

@ESWZY
Last active May 9, 2024 17:48
Show Gist options
  • Save ESWZY/a420a308d3118f21274a0bc3a6feb1ff to your computer and use it in GitHub Desktop.
Save ESWZY/a420a308d3118f21274a0bc3a6feb1ff to your computer and use it in GitHub Desktop.
An example Python code for compressing video file to target size.
# 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)
@Inge1234567890
Copy link

Inge1234567890 commented Aug 6, 2022

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'

@ESWZY
Copy link
Author

ESWZY commented Aug 6, 2022

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.🧐

@Inge1234567890
Copy link

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

@ESWZY
Copy link
Author

ESWZY commented Aug 6, 2022

@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.

@davidraider
Copy link

@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
Copy link
Author

ESWZY commented Jun 27, 2023

@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

https://github.com/althonos/ffpb/blob/da41f96433bfc1d48a3ba4523d647ea1dac6a44e/ffpb.py#L155C21-L155C27

Or, you can parse the output of this Python snippet by following answers:

https://stackoverflow.com/questions/747982/can-ffmpeg-show-a-progress-bar

@leonegao
Copy link

leonegao commented Jan 3, 2024

Do you have any function to compress a video file in Django in production? Thanks

@jmartinezchaine
Copy link

skvideo.io.FFmpegWriter con esta Clase es posible utiliar H264, una fiesta

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment