EDIT4:
As per @Arakis pointed out, what you want (enough screenshots about any given animation to worth making a video) is not possible without taking over the animation in the browser. If you are not able to take over the animation, this is the best you can get:
- load up the page
- while the animation lasts, create as many screenshots as possible
- save the data
import os
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
TARGET_FOLDER = r'C:\test\animated_you_not_wanted\{}.png'
WINDOW_SIZE = 1920, 1080
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("start-maximized")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")
shots = []
if not os.path.isdir(os.path.dirname(TARGET_FOLDER)):
os.makedirs(os.path.dirname(TARGET_FOLDER))
driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe', options=options,
service_args=['--ignore-ssl-errors=true', '--ssl-protocol=any'])
# driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe')
driver.set_window_size(*WINDOW_SIZE)
driver.get('https://daneden.github.io/animate.css/')
# we know that the animation duration on this page is 1 sec so we will try to record as many frames as possible in 1 sec
start_time = time.time()
while time.time() <= start_time + 1:
shots.append(driver.get_screenshot_as_png())
# dumping all captured frames
for i in range(len(shots)):
with open(TARGET_FOLDER.format(i), "wb") as f:
f.write(shots[i])
If you run this code above, you will get approx 3 screenshots, maybe 4 if you have a beast machine.
Why is this happening? When selenium grabs a screenshot, the animation in the browser won't stop and wonder if is it feasible to move on, it just runs independent as normal. Selenium in this resolution (1920x1080) is capable of grabbing 3-4 screenshots in a second. If you reduce the screen resolution to 640x480, you'll get 5-7 screenshots per second depending on your machine, but that frame rate is still very-very far form what you probably desire. Selenium interacts with a browser trough an API but does not control the browser. Taking a screenshot takes time and while selenium grabs the rendered page as an image, animation moves on.
If you want to have a smooth animation with high frame rate, you have to take control over the animation by overriding the animation states. You have to:
- set the
animation-play-state
to paused
- set the
animation-duration
to a reasonable length
- set the
animation-delay
to a negative value to select a given animation state
- in a
for
loop adjust the animation-delay
and grab a screenshot
- save screenshot data later
On the given page (https://daneden.github.io/animate.css/) in the opening animation there are 2 webelements animated with the same animation. So what the code below does is the list above with the little extra: it 'steps' the animation cycle for both elements.
import os
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
TARGET_FOLDER = r'C:\test\animated_you_wanted\{}.png'
WINDOW_SIZE = 1920, 1080
ANIM_DURATION = 2
FRAMES = 60
BASE_SCR = 'arguments[0].setAttribute("style", "display: block;animation-delay: {}s;animation-duration: {}s; animation-iteration-count: infinite;animation-play-state: paused;")'
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("start-maximized")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")
driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe', options=options,
service_args=['--ignore-ssl-errors=true', '--ssl-protocol=any'])
# driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe')
driver.set_window_size(*WINDOW_SIZE)
driver.get('https://daneden.github.io/animate.css/')
header = driver.find_element_by_xpath('//*[@class="site__header island"]')
content = driver.find_element_by_xpath('//*[@class="site__content island"]')
shots = []
for frame in range(FRAMES):
for elem in (header, content):
driver.execute_script(BASE_SCR.format((frame / FRAMES) * -ANIM_DURATION, ANIM_DURATION), elem)
shots.append(driver.get_screenshot_as_png())
if not os.path.isdir(os.path.dirname(TARGET_FOLDER)):
os.makedirs(os.path.dirname(TARGET_FOLDER))
# dumping all captured frames
for i in range(len(shots)):
with open(TARGET_FOLDER.format(i), "wb") as f:
f.write(shots[i])
This is the best you can get with selenium.
BONUS: (or edit5?)
I was wondering if is really impossible to squeeze out more than 5-6 frames per second with a completely generic solution (webpage-independent). I was thinking on this:
- there are JS libraries which are capable of converting html elements into images
- it is possible to inject JS to the page with selenium
- since there are no API calls , a pure JS screenshot logic could outperform the pure selenium approach
So I decided to:
- inject the JS library to the page
- reload the document.body to reset the animation
- grab the screenshots and save them
This is the code, contains more JS than python :D :
import base64
import os
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
TARGET_FOLDER = r'C:\test\animated_you_actually_wanted\{}.png'
WINDOW_SIZE = 800, 600
ANIM_DURATION = 1
FRAMES = 15
BASE_SCR = """
function load_script(){
let ss = document.createElement("script");
ss.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js";
ss.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild(ss);
};
load_script();
shots = [];
window.take = function() {
html2canvas(document.body, {
onrendered: function (canvas) {
shots.push(canvas);
}
})
};
window.record = function(times, sleep){
for (let i=0; i<times; i++){
setTimeout(window.take(), sleep*i)
console.log("issued screenshot with sleep: " + sleep*i)
}
};
""" + """
document.body.setAttribute("style", "width: {width}px; height: {height}px");
""".format(width=WINDOW_SIZE[1], height=WINDOW_SIZE[0])
RECORD_SCR = """
document.body.innerHTML = document.body.innerHTML
window.record({}, {})
"""
GRAB_SCR = """
function getShots(){
let retval = []
for (let i=0; i<shots.length; i++){
retval.push(shots[i].toDataURL("image/png"))
}
return retval;
}
return getShots()
"""
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("start-maximized")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")
if not os.path.isdir(os.path.dirname(TARGET_FOLDER)):
os.makedirs(os.path.dirname(TARGET_FOLDER))
driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe', options=options,
service_args=['--ignore-ssl-errors=true', '--ssl-protocol=any'])
# driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe')
driver.set_window_size(*WINDOW_SIZE)
driver.get('https://daneden.github.io/animate.css/')
driver.execute_script(BASE_SCR)
time.sleep(3)
driver.execute_script(RECORD_SCR.format(FRAMES, (ANIM_DURATION/FRAMES)*100))
shots = []
while len(shots) < FRAMES:
shots = driver.execute_script(GRAB_SCR)
# dumping all captured frames
for i in range(len(shots)):
with open(TARGET_FOLDER.format(i), "wb") as f:
f.write(base64.urlsafe_b64decode(shots[i].split('base64,')[1]))
driver.quit()
Outcome of the html2canvas experiment:
- the sweet-spot regarding frame rate is around 15-ish on the page mentioned above. More frames cause bottlenecks, especially in the first few frames where the text float-in initial position increases the document.body size
- target element (document.body) dimension heavily impacts the performance
- bottlenecks everywhere. I've tried to avoid magic numbers, wasn't easy
- rendered image may contain glitches, like texts outside their container
- it may be worth trying a different library, others may perform better
- this result is still lightyears away from the 60fps frame rate one can achieve with a non-generic solution
- one can get more frames in headless mode