File: generate_video_with_pts.py

package info (click to toggle)
python-av 14.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,664 kB
  • sloc: python: 4,712; sh: 175; ansic: 174; makefile: 123
file content (88 lines) | stat: -rw-r--r-- 2,957 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python3

import colorsys
from fractions import Fraction

import numpy as np

import av

(width, height) = (640, 360)
total_frames = 20
fps = 30

container = av.open("generate_video_with_pts.mp4", mode="w")

stream = container.add_stream("mpeg4", rate=fps)  # alibi frame rate
stream.width = width
stream.height = height
stream.pix_fmt = "yuv420p"

# ffmpeg time is complicated
# more at https://github.com/PyAV-Org/PyAV/blob/main/docs/api/time.rst
# our situation is the "encoding" one

# this is independent of the "fps" you give above
# 1/1000 means milliseconds (and you can use that, no problem)
# 1/2 means half a second (would be okay for the delays we use below)
# 1/30 means ~33 milliseconds
# you should use the least fraction that makes sense for you
stream.codec_context.time_base = Fraction(1, fps)

# this says when to show the next frame
# (increment by how long the current frame will be shown)
my_pts = 0  # [seconds]
# below we'll calculate that into our chosen time base

# we'll keep this frame around to draw on this persistently
# you can also redraw into a new object every time but you needn't
the_canvas = np.zeros((height, width, 3), dtype=np.uint8)
the_canvas[:, :] = (32, 32, 32)  # some dark gray background because why not
block_w2 = int(0.5 * width / total_frames * 0.75)
block_h2 = int(0.5 * height / 4)

for frame_i in range(total_frames):
    # move around the color wheel (hue)
    nice_color = colorsys.hsv_to_rgb(frame_i / total_frames, 1.0, 1.0)
    nice_color = (np.array(nice_color) * 255).astype(np.uint8)

    # draw blocks of a progress bar
    cx = int(width / total_frames * (frame_i + 0.5))
    cy = int(height / 2)
    the_canvas[cy - block_h2 : cy + block_h2, cx - block_w2 : cx + block_w2] = (
        nice_color
    )

    frame = av.VideoFrame.from_ndarray(the_canvas, format="rgb24")

    # seconds -> counts of time_base
    frame.pts = int(round(my_pts / stream.codec_context.time_base))

    # increment by display time to pre-determine next frame's PTS
    my_pts += 1.0 if ((frame_i // 3) % 2 == 0) else 0.5
    # yes, the last frame has no "duration" because nothing follows it
    # frames don't have duration, only a PTS

    for packet in stream.encode(frame):
        container.mux(packet)

# finish it with a blank frame, so the "last" frame actually gets shown for some time
# this black frame will probably be shown for 1/fps time
# at least, that is the analysis of ffprobe
the_canvas[:] = 0
frame = av.VideoFrame.from_ndarray(the_canvas, format="rgb24")
frame.pts = int(round(my_pts / stream.codec_context.time_base))
for packet in stream.encode(frame):
    container.mux(packet)

# the time should now be 15.5 + 1/30 = 15.533

# without that last black frame, the real last frame gets shown for 1/30
# so that video would have been 14.5 + 1/30 = 14.533 seconds long

# Flush stream
for packet in stream.encode():
    container.mux(packet)

# Close the file
container.close()