Skip to content

Commit 6a682f5

Browse files
Add screencast functionality (#494)
1 parent 8a96099 commit 6a682f5

File tree

5 files changed

+238
-0
lines changed

5 files changed

+238
-0
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,54 @@ page.go_to("https://google.com/")
457457
page.mhtml(path: "google.mhtml") # => 87742
458458
```
459459

460+
## Screencast
461+
462+
#### start_screencast(\*\*options) {|data, metadata, session_id| block }
463+
464+
Starts sending each frame to the given block.
465+
466+
* options `Hash`
467+
* :format `Symbol` `:jpeg` | `:png` The format the image should be returned in.
468+
* :quality `Integer` The image quality. **Note:** 0-100 works for JPEG only.
469+
* :max_width `Integer` Maximum screencast frame width.
470+
* :max_height `Integer` Maximum screencast frame height.
471+
* :every_nth_frame `Integer` Send every n-th frame.
472+
473+
* Block inputs:
474+
* data `String` Base64-encoded compressed image.
475+
* metadata `Hash` Screencast frame metadata.
476+
* 'offsetTop' `Integer` Top offset in DIP.
477+
* 'pageScaleFactor' `Integer` Page scale factor.
478+
* 'deviceWidth' `Integer` Device screen width in DIP.
479+
* 'deviceHeight' `Integer` Device screen height in DIP.
480+
* 'scrollOffsetX' `Integer` Position of horizontal scroll in CSS pixels.
481+
* 'scrollOffsetY' `Integer` Position of vertical scroll in CSS pixels.
482+
* 'timestamp' `Float` (optional) Frame swap timestamp in seconds since Unix epoch.
483+
* session_id `Integer` Frame number.
484+
485+
```ruby
486+
require 'base64'
487+
488+
page.go_to("https://apple.com/ipad")
489+
490+
page.start_screencast(format: :jpeg, quality: 75) do |data, metadata|
491+
timestamp_ms = metadata['timestamp'] * 1000
492+
File.binwrite("image_#{timestamp_ms.to_i}.jpg", Base64.decode64(data))
493+
end
494+
495+
sleep 10
496+
497+
page.stop_screencast
498+
```
499+
500+
> ### 📝 NOTE
501+
>
502+
> Chrome only sends new frames while page content is changing. For example, if
503+
> there is an animation or a video on the page, Chrome sends frames at the rate
504+
> requested. On the other hand, if the page is nothing but a wall of static text,
505+
> Chrome sends frames while the page renders. Once Chrome has finished rendering
506+
> the page, it sends no more frames until something changes (e.g., navigating to
507+
> another location).
460508
461509
## Network
462510

lib/ferrum/browser.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Browser
2323
headers cookies network downloads
2424
mouse keyboard
2525
screenshot pdf mhtml viewport_size device_pixel_ratio
26+
start_screencast stop_screencast
2627
frames frame_by main_frame
2728
evaluate evaluate_on evaluate_async execute evaluate_func
2829
add_script_tag add_style_tag bypass_csp

lib/ferrum/page.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require "ferrum/network"
1111
require "ferrum/downloads"
1212
require "ferrum/page/frames"
13+
require "ferrum/page/screencast"
1314
require "ferrum/page/screenshot"
1415
require "ferrum/page/animation"
1516
require "ferrum/page/tracing"
@@ -27,6 +28,7 @@ class Page
2728
delegate %i[base_url default_user_agent timeout timeout=] => :@options
2829

2930
include Animation
31+
include Screencast
3032
include Screenshot
3133
include Frames
3234
include Stream

lib/ferrum/page/screencast.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# frozen_string_literal: true
2+
3+
module Ferrum
4+
class Page
5+
module Screencast
6+
# Starts yielding each frame to the given block.
7+
#
8+
# @param [Hash{Symbol => Object}] opts
9+
#
10+
# @option opts [:jpeg, :png] :format
11+
# The format the image should be returned in.
12+
#
13+
# @option opts [Integer] :quality
14+
# The image quality. **Note:** 0-100 works for JPEG only.
15+
#
16+
# @option opts [Integer] :max_width
17+
# Maximum screencast frame width.
18+
#
19+
# @option opts [Integer] :max_height
20+
# Maximum screencast frame height.
21+
#
22+
# @option opts [Integer] :every_nth_frame
23+
# Send every n-th frame.
24+
#
25+
# @yield [data, metadata, session_id]
26+
# The given block receives the screencast frame along with metadata
27+
# about the frame and the screencast session ID.
28+
#
29+
# @yieldparam data [String]
30+
# Base64-encoded compressed image.
31+
#
32+
# @yieldparam metadata [Hash{String => Object}]
33+
# Screencast frame metadata.
34+
#
35+
# @option metadata [Integer] 'offsetTop'
36+
# Top offset in DIP.
37+
#
38+
# @option metadata [Integer] 'pageScaleFactor'
39+
# Page scale factor.
40+
#
41+
# @option metadata [Integer] 'deviceWidth'
42+
# Device screen width in DIP.
43+
#
44+
# @option metadata [Integer] 'deviceHeight'
45+
# Device screen height in DIP.
46+
#
47+
# @option metadata [Integer] 'scrollOffsetX'
48+
# Position of horizontal scroll in CSS pixels.
49+
#
50+
# @option metadata [Integer] 'scrollOffsetY'
51+
# Position of vertical scroll in CSS pixels.
52+
#
53+
# @option metadata [Float] 'timestamp'
54+
# (optional) Frame swap timestamp in seconds since Unix epoch.
55+
#
56+
# @yieldparam session_id [Integer]
57+
# Frame number.
58+
#
59+
# @example
60+
# require 'base64'
61+
#
62+
# page.go_to("https://apple.com/ipad")
63+
#
64+
# page.start_screencast(format: :jpeg, quality: 75) do |data, metadata|
65+
# timestamp_ms = metadata['timestamp'] * 1000
66+
# File.binwrite("image_#{timestamp_ms.to_i}.jpg", Base64.decode64(data))
67+
# end
68+
#
69+
# sleep 10
70+
#
71+
# page.stop_screencast
72+
#
73+
def start_screencast(**opts)
74+
options = opts.transform_keys { START_SCREENCAST_KEY_CONV.fetch(_1, _1) }
75+
response = command("Page.startScreencast", **options)
76+
77+
if (error_text = response["errorText"]) # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
78+
raise "Starting screencast failed (#{error_text})"
79+
end
80+
81+
on("Page.screencastFrame") do |params|
82+
data, metadata, session_id = params.values_at("data", "metadata", "sessionId")
83+
84+
command("Page.screencastFrameAck", sessionId: session_id)
85+
86+
yield data, metadata, session_id
87+
end
88+
end
89+
90+
# Stops sending each frame.
91+
def stop_screencast
92+
command("Page.stopScreencast")
93+
end
94+
95+
START_SCREENCAST_KEY_CONV = {
96+
max_width: :maxWidth,
97+
max_height: :maxHeight,
98+
every_nth_frame: :everyNthFrame
99+
}.freeze
100+
end
101+
end
102+
end

spec/page/screencast_spec.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
require "base64"
4+
require "image_size"
5+
require "pdf/reader"
6+
require "chunky_png"
7+
require "ferrum/rgba"
8+
9+
describe Ferrum::Page::Screencast do
10+
after(:example) do
11+
browser.stop_screencast
12+
13+
Dir.glob("#{PROJECT_ROOT}/spec/tmp/screencast_frame*") { File.delete _1 }
14+
end
15+
16+
describe "#start_screencast" do
17+
context "when the page has no changing content" do
18+
it "should continue screencasting frames" do
19+
browser.go_to "/ferrum/long_page"
20+
21+
format = :jpeg
22+
count = 0
23+
browser.start_screencast(format: format) do |data, _metadata, _session_id|
24+
count += 1
25+
path = "#{PROJECT_ROOT}/spec/tmp/screencast_frame_#{format('%05d', count)}.#{format}"
26+
File.binwrite(path, Base64.decode64(data))
27+
end
28+
29+
sleep 5
30+
31+
expect(Dir.glob("#{PROJECT_ROOT}/spec/tmp/screencast_frame_*").count).to be_positive.and be < 5
32+
33+
browser.stop_screencast
34+
end
35+
end
36+
37+
context "when the page content continually changes" do
38+
it "should stop screencasting frames when the page has finished rendering" do
39+
browser.go_to "/ferrum/animation"
40+
41+
format = :jpeg
42+
count = 0
43+
browser.start_screencast(format: format) do |data, _metadata, _session_id|
44+
count += 1
45+
path = "#{PROJECT_ROOT}/spec/tmp/screencast_frame_#{format('%05d', count)}.#{format}"
46+
File.binwrite(path, Base64.decode64(data))
47+
end
48+
49+
sleep 5
50+
51+
expect(Dir.glob("#{PROJECT_ROOT}/spec/tmp/screencast_frame_*").count).to be > 250
52+
53+
browser.stop_screencast
54+
end
55+
end
56+
end
57+
58+
describe "#stop_screencast" do
59+
context "when the page content continually changes" do
60+
it "should stop screencasting frames when the page has finished rendering" do
61+
browser.go_to "/ferrum/animation"
62+
63+
format = :jpeg
64+
count = 0
65+
browser.start_screencast(format: format) do |data, _metadata, _session_id|
66+
count += 1
67+
path = "#{PROJECT_ROOT}/spec/tmp/screencast_frame_#{format('%05d', count)}.#{format}"
68+
File.binwrite(path, Base64.decode64(data))
69+
end
70+
71+
sleep 5
72+
73+
browser.stop_screencast
74+
75+
number_of_frames_after_stop = Dir.glob("#{PROJECT_ROOT}/spec/tmp/screencast_frame_*").count
76+
77+
sleep 2
78+
79+
number_of_frames_after_delay = Dir.glob("#{PROJECT_ROOT}/spec/tmp/screencast_frame_*").count
80+
81+
expect(number_of_frames_after_stop).to be <= number_of_frames_after_delay
82+
end
83+
end
84+
end
85+
end

0 commit comments

Comments
 (0)