spectator_sport 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -6
- data/app/controllers/spectator_sport/dashboard/dashboards_controller.rb +1 -0
- data/app/controllers/spectator_sport/dashboard/recordings_controller.rb +50 -0
- data/app/controllers/spectator_sport/dashboard/session_windows_controller.rb +9 -1
- data/app/controllers/spectator_sport/dashboard/tags_controller.rb +8 -5
- data/app/controllers/spectator_sport/events_controller.rb +64 -8
- data/app/frontend/spectator_sport/dashboard/modules/player_controller.js +4 -0
- data/app/frontend/spectator_sport/dashboard/vendor/rrweb-player/rrweb-player.min.css +1 -1
- data/app/frontend/spectator_sport/dashboard/vendor/rrweb-player/rrweb-player.min.js +7 -10
- data/app/helpers/spectator_sport/script_helper.rb +15 -2
- data/app/models/spectator_sport/event.rb +3 -2
- data/app/models/spectator_sport/label.rb +58 -0
- data/app/models/spectator_sport/recording.rb +17 -0
- data/app/models/spectator_sport/recording_analysis.rb +11 -0
- data/app/models/spectator_sport/search_query/lexer.rb +128 -0
- data/app/models/spectator_sport/search_query/parser.rb +131 -0
- data/app/models/spectator_sport/search_query/syntax_tree.rb +152 -0
- data/app/models/spectator_sport/search_query.rb +97 -0
- data/app/models/spectator_sport/session_window.rb +3 -2
- data/app/views/spectator_sport/dashboard/dashboards/index.html.erb +1 -48
- data/app/views/spectator_sport/dashboard/recordings/_stream_events_frame.erb +1 -0
- data/app/views/spectator_sport/dashboard/recordings/details.html.erb +22 -0
- data/app/views/spectator_sport/dashboard/recordings/index.html.erb +24 -0
- data/app/views/spectator_sport/dashboard/recordings/show.html.erb +69 -0
- data/app/views/spectator_sport/dashboard/recordings/stream_events.erb +13 -0
- data/app/views/spectator_sport/dashboard/session_windows/index.html.erb +7 -0
- data/app/views/spectator_sport/dashboard/session_windows/show.html.erb +54 -20
- data/app/views/spectator_sport/dashboard/shared/_recordings.html.erb +47 -0
- data/app/views/spectator_sport/dashboard/shared/_session_windows.html.erb +48 -0
- data/app/views/spectator_sport/dashboard/tags/show.html.erb +1 -23
- data/app/views/spectator_sport/events/index.js +327 -116
- data/app/views/spectator_sport/shared/_navbar.erb +6 -1
- data/config/dashboard_routes.rb +8 -2
- data/lib/spectator_sport/engine.rb +7 -3
- data/lib/spectator_sport/version.rb +1 -1
- data/lib/spectator_sport.rb +5 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae07042f4d65dd617066efe9e6ac3f90f6a8726dec741b9e3965c6d76eef1648
|
|
4
|
+
data.tar.gz: 7c2fc362efbea72b71dc61b99568577d554e47f5b4a63e77298af5e5879355c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35962025a9098a0d9fe0a1e113ee8c9036a4c24f6cd7f1fa7fddb00c8fc923eba9102f54fbfe56e2f4080704dacc27762d18e59845e6257ff4a4e3decbb18b97
|
|
7
|
+
data.tar.gz: 2eb708af1cf7e0e59ec60aa82b5940846869e36d91834b9a63e3d59b8d8b20f180fc030576deb34e3d4fb91fc9b81baca46e582a89acf45e28d5eadebe5bfcab
|
data/README.md
CHANGED
|
@@ -38,7 +38,7 @@ To install Spectator Sport in your Rails application:
|
|
|
38
38
|
bundle add spectator_sport
|
|
39
39
|
```
|
|
40
40
|
2. Install Spectator Sport in your application. _🚧 This will change on the path to v1._ Explore the `/demo` app as live example:
|
|
41
|
-
- Create database migrations with `bin/rails
|
|
41
|
+
- Create database migrations with `bin/rails spectator_sport:install:migrations`. Apply migrations with `bin/rails db:prepare`
|
|
42
42
|
- Mount the recorder API in your application's routes with `mount SpectatorSport::Engine, at: "/spectator_sport, as: :spectator_sport"`
|
|
43
43
|
- Add the `spectator_sport_script_tags` helper to the bottom of the `<head>` of `layout/application.rb`. Example:
|
|
44
44
|
```erb
|
|
@@ -57,17 +57,37 @@ To install Spectator Sport in your Rails application:
|
|
|
57
57
|
```
|
|
58
58
|
3. To view recordings, you will want to mount the Player Dashboard in your application and set up authorization to limit access. See the section on [Dashboard authorization](#dashboard-authorization) for instructions.
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## Labeling recordings
|
|
61
61
|
|
|
62
|
-
You can associate
|
|
62
|
+
You can associate key-value labels with the current recording by calling `spectator_sport_label_recording` in any template:
|
|
63
63
|
|
|
64
64
|
```erb
|
|
65
|
-
<%=
|
|
65
|
+
<%= spectator_sport_label_recording(current_user.id.to_s, key: "user_id", strategy: :one) %>
|
|
66
|
+
<%= spectator_sport_label_recording("admin", key: "role", strategy: :many) %>
|
|
67
|
+
<%= spectator_sport_label_recording("vip") %>
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
The `key` argument is optional. When omitted, the label behaves like a tag. The `strategy` argument (default `:many`) controls how values are stored per recording:
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
- `strategy: :many` (default): multiple values for the same key are accumulated per recording.
|
|
73
|
+
- `strategy: :one`: only one value per key is kept per recording. A later call with the same key replaces the stored value. Without a key, behaves like `:many`.
|
|
74
|
+
- `strategy: :first`: only the first value for a key is stored; subsequent calls with the same key are ignored. Requires a key.
|
|
75
|
+
|
|
76
|
+
This renders a hidden `<meta>` element signed by the server. The recording client detects it and immediately sends it to the API, where the signature is verified before the label is stored. Labels are displayed in the dashboard and can be used to look up all recordings associated with a given key-value pair.
|
|
77
|
+
|
|
78
|
+
**Note:** this requires the `spectator_sport_labels` migration to be applied (`bin/rails spectator_sport:install:migrations && bin/rails db:migrate`). If the migration hasn't been run, the feature is silently disabled.
|
|
79
|
+
|
|
80
|
+
## Stopping recording
|
|
81
|
+
|
|
82
|
+
You can pause recording for a page by calling `spectator_sport_stop_recording` in any template:
|
|
83
|
+
|
|
84
|
+
```erb
|
|
85
|
+
<%= spectator_sport_stop_recording %>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This renders a hidden `<meta>` element that the recording client detects on page load. When present, rrweb recording is stopped and no events are buffered or sent. Recording automatically resumes when the user navigates to a page that does not have this tag.
|
|
89
|
+
|
|
90
|
+
This is useful when navigating via Turbo to pages that shouldn't be recorded — without this tag the recorder would continue running across navigations.
|
|
71
91
|
|
|
72
92
|
## Dashboard authorization
|
|
73
93
|
|
|
@@ -153,6 +173,21 @@ open http://localhost:3000
|
|
|
153
173
|
# 6. Make changes, see the result, commit and make a PR!
|
|
154
174
|
```
|
|
155
175
|
|
|
176
|
+
## Releasing a new version
|
|
177
|
+
|
|
178
|
+
1. Update the version in `lib/spectator_sport/version.rb`
|
|
179
|
+
2. Run `bundle install` to update `Gemfile.lock`
|
|
180
|
+
3. Commit the version bump and updated `Gemfile.lock`:
|
|
181
|
+
```bash
|
|
182
|
+
git add lib/spectator_sport/version.rb Gemfile.lock
|
|
183
|
+
git commit -m "Bump version to x.y.z"
|
|
184
|
+
```
|
|
185
|
+
3. Build and publish to RubyGems, tag, and push to GitHub:
|
|
186
|
+
```bash
|
|
187
|
+
bundle exec rake release
|
|
188
|
+
```
|
|
189
|
+
4. Create a GitHub Release at https://github.com/bensheldon/spectator_sport/releases using the new `vx.y.z` tag.
|
|
190
|
+
|
|
156
191
|
## License
|
|
157
192
|
|
|
158
193
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -4,6 +4,7 @@ module SpectatorSport
|
|
|
4
4
|
def index
|
|
5
5
|
@session_windows = SessionWindow.order(:created_at).limit(50).reverse_order
|
|
6
6
|
@session_windows = @session_windows.includes(:session_window_tags) if SpectatorSport::SessionWindowTag.migrated?
|
|
7
|
+
@session_windows = @session_windows.includes(:labels) if SpectatorSport::Label.migrated?
|
|
7
8
|
end
|
|
8
9
|
end
|
|
9
10
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module SpectatorSport
|
|
2
|
+
module Dashboard
|
|
3
|
+
class RecordingsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@search_query = SpectatorSport::SearchQuery.new(query: params[:query])
|
|
6
|
+
|
|
7
|
+
@recordings = Recording.order(:created_at).limit(50).reverse_order
|
|
8
|
+
@recordings = @recordings.includes(:labels) if SpectatorSport::Label.migrated?
|
|
9
|
+
|
|
10
|
+
if @search_query.query.present? && SpectatorSport::Label.migrated?
|
|
11
|
+
if @search_query.valid?
|
|
12
|
+
@recordings = @search_query.to_scope(base_scope: @recordings)
|
|
13
|
+
else
|
|
14
|
+
@recordings = @recordings.none
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def show
|
|
20
|
+
@recording = Recording.find(params[:id])
|
|
21
|
+
@events = @recording.events.page_after(nil)
|
|
22
|
+
@labels = SpectatorSport::Label.migrated? ? @recording.labels.to_a : []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stream_events
|
|
26
|
+
response.content_type = "text/vnd.turbo-stream.html"
|
|
27
|
+
return head(:ok) if params[:after_event_id].blank?
|
|
28
|
+
|
|
29
|
+
recording = Recording.find(params[:id])
|
|
30
|
+
previous_event = recording.events.find_by(id: params[:after_event_id])
|
|
31
|
+
@events = recording.events.page_after(previous_event)
|
|
32
|
+
|
|
33
|
+
render layout: false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def details
|
|
37
|
+
@recording = Recording.find(params[:id])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
@recording = Recording.find(params[:id])
|
|
42
|
+
@recording.events.delete_all
|
|
43
|
+
@recording.labels.delete_all if SpectatorSport::Label.migrated?
|
|
44
|
+
@recording.delete
|
|
45
|
+
|
|
46
|
+
redirect_to root_path
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
module SpectatorSport
|
|
2
2
|
module Dashboard
|
|
3
3
|
class SessionWindowsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@session_windows = SessionWindow.order(:created_at).limit(50).reverse_order
|
|
6
|
+
@session_windows = @session_windows.includes(:session_window_tags) if SpectatorSport::SessionWindowTag.migrated?
|
|
7
|
+
@session_windows = @session_windows.includes(:labels) if SpectatorSport::Label.migrated?
|
|
8
|
+
end
|
|
9
|
+
|
|
4
10
|
def show
|
|
5
11
|
@session_window = SessionWindow.find(params[:id])
|
|
6
12
|
@events = @session_window.events.page_after(nil)
|
|
7
|
-
@
|
|
13
|
+
@labels = SpectatorSport::Label.migrated? ? @session_window.labels.to_a : []
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
def events
|
|
@@ -25,6 +31,8 @@ module SpectatorSport
|
|
|
25
31
|
def destroy
|
|
26
32
|
@session_window = SessionWindow.find(params[:id])
|
|
27
33
|
@session_window.events.delete_all
|
|
34
|
+
@session_window.session_window_tags.delete_all if SpectatorSport::SessionWindowTag.migrated?
|
|
35
|
+
@session_window.labels.delete_all if SpectatorSport::Label.migrated?
|
|
28
36
|
@session_window.delete
|
|
29
37
|
|
|
30
38
|
redirect_to root_path
|
|
@@ -3,12 +3,15 @@ module SpectatorSport
|
|
|
3
3
|
class TagsController < ApplicationController
|
|
4
4
|
def show
|
|
5
5
|
@tag = params[:name]
|
|
6
|
-
|
|
7
|
-
.
|
|
8
|
-
.
|
|
9
|
-
.
|
|
6
|
+
scope = SessionWindow
|
|
7
|
+
.joins(:session_window_tags)
|
|
8
|
+
.where(spectator_sport_session_window_tags: { tag: @tag })
|
|
9
|
+
.distinct
|
|
10
|
+
.order(updated_at: :desc)
|
|
10
11
|
.limit(50)
|
|
11
|
-
|
|
12
|
+
scope = scope.includes(:session_window_tags) if SpectatorSport::SessionWindowTag.migrated?
|
|
13
|
+
scope = scope.includes(:labels) if SpectatorSport::Label.migrated?
|
|
14
|
+
@session_windows = scope
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
end
|
|
@@ -6,19 +6,61 @@ module SpectatorSport
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def create
|
|
9
|
-
data = if params.key?(:
|
|
10
|
-
params.slice(:sessionId, :windowId, :events, :tags).stringify_keys
|
|
9
|
+
data = if params.key?(:events) && (params.key?(:recordingId) || params.key?(:windowId))
|
|
10
|
+
params.slice(:sessionId, :recordingId, :windowId, :events, :tags, :labels).stringify_keys
|
|
11
11
|
else
|
|
12
12
|
# beacon sends JSON in the request body
|
|
13
|
-
JSON.parse(request.body.read).slice("sessionId", "windowId", "events", "tags")
|
|
13
|
+
JSON.parse(request.body.read).slice("sessionId", "recordingId", "windowId", "events", "tags", "labels")
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# `windowId` is the legacy name for `recordingId`; accept either for backward compatibility.
|
|
17
|
+
data["recordingId"] ||= data["windowId"]
|
|
18
|
+
data["windowId"] ||= data["recordingId"]
|
|
19
|
+
|
|
18
20
|
events = data["events"]
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
if SpectatorSport::Recording.migrated?
|
|
23
|
+
create_recording(data, events)
|
|
24
|
+
else
|
|
25
|
+
create_session_window(data, events)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
render json: { message: "ok" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def create_recording(data, events)
|
|
34
|
+
recording = Recording.find_or_create_by(secure_id: data["recordingId"])
|
|
35
|
+
|
|
36
|
+
records_data = events.map do |event|
|
|
37
|
+
{ recording_id: recording.id, event_data: event, created_at: Time.at(event["timestamp"].to_f / 1000.0) }
|
|
38
|
+
end.to_a
|
|
39
|
+
Event.insert_all(records_data) if records_data.any?
|
|
40
|
+
|
|
41
|
+
last_event = records_data.max_by { |record| record[:created_at] }
|
|
42
|
+
recording.update(updated_at: last_event[:created_at]) if last_event
|
|
43
|
+
|
|
44
|
+
if SpectatorSport::Label.migrated?
|
|
45
|
+
verifier = Rails.application.message_verifier(:spectator_sport_label_recording)
|
|
46
|
+
Array(data["labels"]).first(20).each do |signed_label|
|
|
47
|
+
label_data = verifier.verified(signed_label)
|
|
48
|
+
next unless label_data.is_a?(Hash)
|
|
49
|
+
Label.record(
|
|
50
|
+
recording: recording,
|
|
51
|
+
value: label_data["value"],
|
|
52
|
+
key: label_data["key"],
|
|
53
|
+
strategy: label_data["strategy"]
|
|
54
|
+
)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_session_window(data, events)
|
|
62
|
+
session = Session.find_or_create_by(secure_id: data["sessionId"])
|
|
63
|
+
window = SessionWindow.find_or_create_by(secure_id: data["windowId"], session: session)
|
|
22
64
|
|
|
23
65
|
records_data = events.map do |event|
|
|
24
66
|
{ session_id: session.id, session_window_id: window.id, event_data: event, created_at: Time.at(event["timestamp"].to_f / 1000.0) }
|
|
@@ -39,7 +81,21 @@ module SpectatorSport
|
|
|
39
81
|
end
|
|
40
82
|
end
|
|
41
83
|
|
|
42
|
-
|
|
84
|
+
if SpectatorSport::Label.migrated?
|
|
85
|
+
verifier = Rails.application.message_verifier(:spectator_sport_label_recording)
|
|
86
|
+
Array(data["labels"]).first(20).each do |signed_label|
|
|
87
|
+
label_data = verifier.verified(signed_label)
|
|
88
|
+
next unless label_data.is_a?(Hash)
|
|
89
|
+
Label.record(
|
|
90
|
+
session_window: window,
|
|
91
|
+
value: label_data["value"],
|
|
92
|
+
key: label_data["key"],
|
|
93
|
+
strategy: label_data["strategy"]
|
|
94
|
+
)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
43
99
|
end
|
|
44
100
|
end
|
|
45
101
|
end
|
|
@@ -10,11 +10,15 @@ export default class extends Controller {
|
|
|
10
10
|
static targets = [ "player", "events", "linkUrl" ]
|
|
11
11
|
|
|
12
12
|
connect() {
|
|
13
|
+
if (this.eventsValue.length === 0) return;
|
|
14
|
+
|
|
13
15
|
this.player = new Player({
|
|
14
16
|
target: this.playerTarget,
|
|
15
17
|
props: {
|
|
16
18
|
events: this.eventsValue,
|
|
17
19
|
liveMode: true,
|
|
20
|
+
skipInactive: false,
|
|
21
|
+
speedOption: [1, 2, 4, 8, 16],
|
|
18
22
|
}
|
|
19
23
|
});
|
|
20
24
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// https://cdn.jsdelivr.net/npm/rrweb-player@2.0.0-alpha.18/dist/style.min.css
|
|
2
1
|
.switch.svelte-a6h7w7.svelte-a6h7w7.svelte-a6h7w7{height:1em;display:flex;align-items:center}.switch.disabled.svelte-a6h7w7.svelte-a6h7w7.svelte-a6h7w7{opacity:.5}.label.svelte-a6h7w7.svelte-a6h7w7.svelte-a6h7w7{margin:0 8px}.switch.svelte-a6h7w7 input[type=checkbox].svelte-a6h7w7.svelte-a6h7w7{position:absolute;opacity:0}.switch.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7{width:2em;height:1em;position:relative;cursor:pointer;display:block}.switch.disabled.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7{cursor:not-allowed}.switch.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7:before{content:"";position:absolute;width:2em;height:1em;left:.1em;transition:background .1s ease;background:#4950f680;border-radius:50px}.switch.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7:after{content:"";position:absolute;width:1em;height:1em;border-radius:50px;left:0;transition:all .2s ease;box-shadow:0 2px 5px #0000004d;background:#fcfff4;animation:switch-off .2s ease-out;z-index:2}.switch.svelte-a6h7w7 input[type=checkbox].svelte-a6h7w7:checked+label.svelte-a6h7w7:before{background:#4950f6}.switch.svelte-a6h7w7 input[type=checkbox].svelte-a6h7w7:checked+label.svelte-a6h7w7:after{animation:switch-on .2s ease-out;left:1.1em}.rr-controller.svelte-189zk2r.svelte-189zk2r{width:100%;height:80px;background:#fff;display:flex;flex-direction:column;justify-content:space-around;align-items:center;border-radius:0 0 5px 5px}.rr-timeline.svelte-189zk2r.svelte-189zk2r{width:80%;display:flex;align-items:center}.rr-timeline__time.svelte-189zk2r.svelte-189zk2r{display:inline-block;width:100px;text-align:center;color:#11103e}.rr-progress.svelte-189zk2r.svelte-189zk2r{flex:1;height:12px;background:#eee;position:relative;border-radius:3px;cursor:pointer;box-sizing:border-box;border-top:solid 4px #fff;border-bottom:solid 4px #fff}.rr-progress.disabled.svelte-189zk2r.svelte-189zk2r{cursor:not-allowed}.rr-progress__step.svelte-189zk2r.svelte-189zk2r{height:100%;position:absolute;left:0;top:0;background:#e0e1fe}.rr-progress__handler.svelte-189zk2r.svelte-189zk2r{width:20px;height:20px;border-radius:10px;position:absolute;top:2px;transform:translate(-50%,-50%);background:#4950f6}.rr-controller__btns.svelte-189zk2r.svelte-189zk2r{display:flex;align-items:center;justify-content:center;font-size:13px}.rr-controller__btns.svelte-189zk2r button.svelte-189zk2r{width:32px;height:32px;display:flex;padding:0;align-items:center;justify-content:center;background:none;border:none;border-radius:50%;cursor:pointer}.rr-controller__btns.svelte-189zk2r button.svelte-189zk2r:active{background:#e0e1fe}.rr-controller__btns.svelte-189zk2r button.active.svelte-189zk2r{color:#fff;background:#4950f6}.rr-controller__btns.svelte-189zk2r button.svelte-189zk2r:disabled{cursor:not-allowed}.replayer-wrapper{position:relative}.replayer-mouse{position:absolute;width:20px;height:20px;transition:left .05s linear,top .05s linear;background-size:contain;background-position:center center;background-repeat:no-repeat;background-image:url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDUwIDUwIiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPjxwYXRoIGQ9Ik00OC43MSw0Mi45MUwzNC4wOCwyOC4yOSw0NC4zMywxOEExLDEsMCwwLDAsNDQsMTYuMzlMMi4zNSwxLjA2QTEsMSwwLDAsMCwxLjA2LDIuMzVMMTYuMzksNDRhMSwxLDAsMCwwLDEuNjUuMzZMMjguMjksMzQuMDgsNDIuOTEsNDguNzFhMSwxLDAsMCwwLDEuNDEsMGw0LjM4LTQuMzhBMSwxLDAsMCwwLDQ4LjcxLDQyLjkxWm0tNS4wOSwzLjY3TDI5LDMyYTEsMSwwLDAsMC0xLjQxLDBsLTkuODUsOS44NUwzLjY5LDMuNjlsMzguMTIsMTRMMzIsMjcuNThBMSwxLDAsMCwwLDMyLDI5TDQ2LjU5LDQzLjYyWiI+PC9wYXRoPjwvc3ZnPg==);border-color:transparent}.replayer-mouse:after{content:"";display:inline-block;width:20px;height:20px;background:#4950f6;border-radius:100%;transform:translate(-50%,-50%);opacity:.3}.replayer-mouse.active:after{animation:click .2s ease-in-out 1}.replayer-mouse.touch-device{background-image:none;width:70px;height:70px;border-width:4px;border-style:solid;border-radius:100%;margin-left:-37px;margin-top:-37px;border-color:#4950f600;transition:left 0s linear,top 0s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device.touch-active{border-color:#4950f6;transition:left .25s linear,top .25s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device:after{opacity:0}.replayer-mouse.touch-device.active:after{animation:touch-click .2s ease-in-out 1}.replayer-mouse-tail{position:absolute;pointer-events:none}@keyframes click{0%{opacity:.3;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}@keyframes touch-click{0%{opacity:0;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}.rr-player{position:relative;background:#fff;float:left;border-radius:5px;box-shadow:0 24px 48px #11103e1f}.rr-player__frame{overflow:hidden}.replayer-wrapper{float:left;clear:both;transform-origin:top left;left:50%;top:50%}.replayer-wrapper>iframe{border:none}
|
|
2
|
+
/*# sourceMappingURL=style.min.css.map */
|