@cntrl-site/sdk-nextjs 0.26.2 → 0.27.1
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.
- package/lib/components/Item.js +4 -5
- package/lib/components/ScrollPlaybackVideo.js +2 -2
- package/lib/components/useItemPosition.js +1 -5
- package/lib/utils/RichTextConverter/RichTextConverter.js +11 -15
- package/package.json +3 -5
- package/src/components/Item.tsx +4 -6
- package/src/components/ScrollPlaybackVideo.tsx +1 -1
- package/src/components/useItemPosition.ts +2 -6
- package/src/utils/RichTextConverter/RichTextConverter.tsx +10 -12
- package/.idea/inspectionProfiles/Project_Default.xml +0 -15
- package/lib/utils/PlaybackVideoConverter/ScrollPlaybackVideoManager.js +0 -221
- package/lib/utils/PlaybackVideoConverter/VideoDecoder.js +0 -175
- package/src/utils/PlaybackVideoConverter/ScrollPlaybackVideoManager.ts +0 -211
- package/src/utils/PlaybackVideoConverter/VideoDecoder.ts +0 -174
package/lib/components/Item.js
CHANGED
|
@@ -55,7 +55,7 @@ const Item = ({ item, sectionId, articleHeight }) => {
|
|
|
55
55
|
const [wrapperHeight, setWrapperHeight] = (0, react_1.useState)(undefined);
|
|
56
56
|
const [itemHeight, setItemHeight] = (0, react_1.useState)(undefined);
|
|
57
57
|
const { scale, scaleAnchor } = (0, useItemScale_1.useItemScale)(item, sectionId);
|
|
58
|
-
const { top, left
|
|
58
|
+
const { top, left } = (0, useItemPosition_1.useItemPosition)(item, sectionId);
|
|
59
59
|
const sectionHeight = (0, useSectionHeightMap_1.useSectionHeightData)(sectionId);
|
|
60
60
|
const stickyTop = (0, useStickyItemTop_1.useStickyItemTop)(item, sectionHeight, sectionId);
|
|
61
61
|
const { width, height } = (0, useItemDimensions_1.useItemDimensions)(item, sectionId);
|
|
@@ -99,7 +99,7 @@ const Item = ({ item, sectionId, articleHeight }) => {
|
|
|
99
99
|
top: `${stickyTop * 100}vw`,
|
|
100
100
|
height: isRichText && itemHeight ? `${itemHeight * 100}vw` : 'unset'
|
|
101
101
|
};
|
|
102
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: `item-wrapper-${item.id}`, ref: itemWrapperRef, style: isInitialRef.current ? {} : Object.assign({ top, left
|
|
102
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: `item-wrapper-${item.id}`, ref: itemWrapperRef, style: isInitialRef.current ? {} : Object.assign({ top, left }, (wrapperHeight !== undefined ? { height: `${wrapperHeight * 100}vw` } : {})), children: [(0, jsx_runtime_1.jsx)("div", { suppressHydrationWarning: true, className: `item-${item.id}`, style: isInitialRef.current ? {} : styles, children: (0, jsx_runtime_1.jsx)(RichTextWrapper, { isRichText: isRichText, children: (0, jsx_runtime_1.jsx)("div", { className: `item-${item.id}-inner`, style: {
|
|
103
103
|
width: `${sizingAxis.x === 'manual'
|
|
104
104
|
? isRichText
|
|
105
105
|
? `${width * exemplary}px`
|
|
@@ -111,12 +111,12 @@ const Item = ({ item, sectionId, articleHeight }) => {
|
|
|
111
111
|
}, children: (0, jsx_runtime_1.jsx)(ItemComponent, { item: item, sectionId: sectionId, onResize: handleItemResize, articleHeight: articleHeight }) }) }) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
112
112
|
${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, hidden, hoverParams, sticky, sectionHeight, layoutParams]) => {
|
|
113
113
|
const sizingAxis = parseSizing(layoutParams.sizing);
|
|
114
|
-
const isScreenBasedBottom = area.positionType === sdk_1.PositionType.ScreenBased && area.anchorSide === sdk_1.AnchorSide.Bottom;
|
|
115
114
|
return (`
|
|
116
115
|
.item-${item.id} {
|
|
117
116
|
position: ${sticky ? 'sticky' : 'absolute'};
|
|
118
117
|
top: ${sticky ? `${(0, getAnchoredItemTop_1.getAnchoredItemTop)(area.top - sticky.from, sectionHeight, area.anchorSide)}` : 0};
|
|
119
118
|
pointer-events: auto;
|
|
119
|
+
cursor: ${hoverParams ? 'pointer' : 'default'};
|
|
120
120
|
display: ${hidden ? 'none' : 'block'};
|
|
121
121
|
height: fit-content;
|
|
122
122
|
}
|
|
@@ -134,8 +134,7 @@ const Item = ({ item, sectionId, articleHeight }) => {
|
|
|
134
134
|
-webkit-transform: translate3d(0, 0, 0);
|
|
135
135
|
transform: translate3d(0, 0, 0);
|
|
136
136
|
pointer-events: none;
|
|
137
|
-
|
|
138
|
-
top: ${isScreenBasedBottom ? 'unset' : (0, getItemTopStyle_1.getItemTopStyle)(area.top, area.anchorSide)};
|
|
137
|
+
top: ${(0, getItemTopStyle_1.getItemTopStyle)(area.top, area.anchorSide)};
|
|
139
138
|
left: ${area.left * 100}vw;
|
|
140
139
|
transition: ${(0, HoverStyles_1.getTransitions)(['left', 'top'], hoverParams)};
|
|
141
140
|
}
|
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ScrollPlaybackVideo = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
|
-
const ScrollPlaybackVideoManager_1 = require("../utils/PlaybackVideoConverter/ScrollPlaybackVideoManager");
|
|
7
6
|
const rangeMap_1 = require("../utils/rangeMap");
|
|
8
7
|
const ArticleRectContext_1 = require("../provider/ArticleRectContext");
|
|
8
|
+
const sdk_1 = require("@cntrl-site/sdk");
|
|
9
9
|
const ScrollPlaybackVideo = ({ sectionId, src, playbackParams, style, className }) => {
|
|
10
10
|
const [containerElement, setContainerElement] = (0, react_1.useState)(null);
|
|
11
11
|
const [time, setTime] = (0, react_1.useState)(0);
|
|
@@ -22,7 +22,7 @@ const ScrollPlaybackVideo = ({ sectionId, src, playbackParams, style, className
|
|
|
22
22
|
const scrollVideoManager = (0, react_1.useMemo)(() => {
|
|
23
23
|
if (!containerElement)
|
|
24
24
|
return null;
|
|
25
|
-
const manager = new
|
|
25
|
+
const manager = new sdk_1.ScrollPlaybackVideoManager({
|
|
26
26
|
src,
|
|
27
27
|
videoContainer: containerElement
|
|
28
28
|
});
|
|
@@ -13,12 +13,8 @@ const useItemPosition = (item, sectionId) => {
|
|
|
13
13
|
return item.area[layoutId];
|
|
14
14
|
}, (animator, scroll, value) => animator.getPositions(value, scroll), sectionId, [layoutId]);
|
|
15
15
|
const anchorSide = layoutId ? item.area[layoutId].anchorSide : sdk_1.AnchorSide.Top;
|
|
16
|
-
const positionType = layoutId ? item.area[layoutId].positionType : sdk_1.PositionType.ScreenBased;
|
|
17
|
-
// tp prevent fixed item (with anchor point bottom) to jump when scroll in safari on mobile
|
|
18
|
-
const isScreenBasedBottom = positionType === sdk_1.PositionType.ScreenBased && anchorSide === sdk_1.AnchorSide.Bottom;
|
|
19
16
|
return {
|
|
20
|
-
|
|
21
|
-
top: isScreenBasedBottom ? 'unset' : (0, getItemTopStyle_1.getItemTopStyle)(top, anchorSide),
|
|
17
|
+
top: (0, getItemTopStyle_1.getItemTopStyle)(top, anchorSide),
|
|
22
18
|
left: `${left * 100}vw`
|
|
23
19
|
};
|
|
24
20
|
};
|
|
@@ -167,19 +167,17 @@ class RichTextConverter {
|
|
|
167
167
|
}
|
|
168
168
|
return styleGroups;
|
|
169
169
|
}
|
|
170
|
-
groupEntities(
|
|
171
|
-
var _a, _b, _c, _d
|
|
170
|
+
groupEntities(entitiesList, styleGroups) {
|
|
171
|
+
var _a, _b, _c, _d;
|
|
172
172
|
const entitiesGroups = [];
|
|
173
|
+
// some entities may have no data, need to filter them out (case with deleting a section/page that was linked to)
|
|
174
|
+
const entities = entitiesList.filter(e => e.hasOwnProperty('data'));
|
|
173
175
|
if (!entities.length && !styleGroups)
|
|
174
176
|
return;
|
|
175
177
|
if (!styleGroups || styleGroups.length === 0) {
|
|
176
|
-
const dividersSet = entities.reduce((ds,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!e.hasOwnProperty('data'))
|
|
180
|
-
return ds;
|
|
181
|
-
ds.add(e.start);
|
|
182
|
-
ds.add(e.end);
|
|
178
|
+
const dividersSet = entities.reduce((ds, s) => {
|
|
179
|
+
ds.add(s.start);
|
|
180
|
+
ds.add(s.end);
|
|
183
181
|
return ds;
|
|
184
182
|
}, new Set([entities[0].start, entities[entities.length - 1].end]));
|
|
185
183
|
const dividers = Array.from(dividersSet).sort((a, b) => a - b);
|
|
@@ -202,11 +200,9 @@ class RichTextConverter {
|
|
|
202
200
|
}
|
|
203
201
|
const start = entities[0].start < styleGroups[0].start ? entities[0].start : styleGroups[0].start;
|
|
204
202
|
const end = entities[entities.length - 1].end > styleGroups[styleGroups.length - 1].end ? entities[entities.length - 1].end : styleGroups[styleGroups.length - 1].end;
|
|
205
|
-
const entitiesDividers = entities.reduce((ds,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
ds.add(e.start);
|
|
209
|
-
ds.add(e.end);
|
|
203
|
+
const entitiesDividers = entities.reduce((ds, s) => {
|
|
204
|
+
ds.add(s.start);
|
|
205
|
+
ds.add(s.end);
|
|
210
206
|
return ds;
|
|
211
207
|
}, new Set([start, end]));
|
|
212
208
|
const entityDividers = Array.from(entitiesDividers).sort((a, b) => a - b);
|
|
@@ -215,7 +211,7 @@ class RichTextConverter {
|
|
|
215
211
|
const end = entityDividers[i + 1];
|
|
216
212
|
const entity = entities.find(e => e.start === start);
|
|
217
213
|
entitiesGroups.push(Object.assign({ stylesGroup: styleGroups.filter(s => s.start >= start && s.end <= end), start,
|
|
218
|
-
end }, (entity && { link:
|
|
214
|
+
end }, (entity && { link: entity.data.url, target: entity.data.target })));
|
|
219
215
|
}
|
|
220
216
|
return entitiesGroups;
|
|
221
217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntrl-site/sdk-nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "SDK for Next.js",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -23,15 +23,13 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@cntrl-site/color": "^1.0.0",
|
|
25
25
|
"@cntrl-site/effects": "^1.0.5",
|
|
26
|
-
"@cntrl-site/sdk": "^1.
|
|
26
|
+
"@cntrl-site/sdk": "^1.9.1",
|
|
27
27
|
"@types/ua-parser-js": "^0.7.39",
|
|
28
28
|
"@types/vimeo__player": "^2.18.0",
|
|
29
29
|
"@vimeo/player": "^2.20.1",
|
|
30
30
|
"html-react-parser": "^3.0.1",
|
|
31
|
-
"mp4box": "^0.5.2",
|
|
32
31
|
"resize-observer-polyfill": "^1.5.1",
|
|
33
|
-
"styled-jsx": "^5.0.2"
|
|
34
|
-
"ua-parser-js": "^1.0.37"
|
|
32
|
+
"styled-jsx": "^5.0.2"
|
|
35
33
|
},
|
|
36
34
|
"peerDependencies": {
|
|
37
35
|
"@types/lodash.isequal": "^4.5.6",
|
package/src/components/Item.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ComponentType, FC, PropsWithChildren, useContext, useEffect, useId, useRef, useState } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
3
|
import {
|
|
4
|
-
AnchorSide,
|
|
5
4
|
ArticleItemType,
|
|
6
5
|
getLayoutStyles,
|
|
7
6
|
Item as TItem,
|
|
@@ -77,7 +76,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
|
|
|
77
76
|
const [wrapperHeight, setWrapperHeight] = useState<undefined | number>(undefined);
|
|
78
77
|
const [itemHeight, setItemHeight] = useState<undefined | number>(undefined);
|
|
79
78
|
const { scale, scaleAnchor } = useItemScale(item, sectionId);
|
|
80
|
-
const { top, left
|
|
79
|
+
const { top, left } = useItemPosition(item, sectionId);
|
|
81
80
|
const sectionHeight = useSectionHeightData(sectionId);
|
|
82
81
|
const stickyTop = useStickyItemTop(item, sectionHeight, sectionId);
|
|
83
82
|
const { width, height } = useItemDimensions(item, sectionId);
|
|
@@ -129,7 +128,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
|
|
|
129
128
|
<div
|
|
130
129
|
className={`item-wrapper-${item.id}`}
|
|
131
130
|
ref={itemWrapperRef}
|
|
132
|
-
style={isInitialRef.current ? {} : { top, left,
|
|
131
|
+
style={isInitialRef.current ? {} : { top, left, ...(wrapperHeight !== undefined ? { height: `${wrapperHeight * 100}vw` } : {}) }}
|
|
133
132
|
>
|
|
134
133
|
<div
|
|
135
134
|
suppressHydrationWarning={true}
|
|
@@ -157,12 +156,12 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
|
|
|
157
156
|
<JSXStyle id={id}>{`
|
|
158
157
|
${getLayoutStyles(layouts, layoutValues, ([area, hidden, hoverParams, sticky, sectionHeight, layoutParams]) => {
|
|
159
158
|
const sizingAxis = parseSizing(layoutParams.sizing);
|
|
160
|
-
const isScreenBasedBottom = area.positionType === PositionType.ScreenBased && area.anchorSide === AnchorSide.Bottom;
|
|
161
159
|
return (`
|
|
162
160
|
.item-${item.id} {
|
|
163
161
|
position: ${sticky ? 'sticky' : 'absolute'};
|
|
164
162
|
top: ${sticky ? `${getAnchoredItemTop(area.top - sticky.from, sectionHeight, area.anchorSide)}` : 0};
|
|
165
163
|
pointer-events: auto;
|
|
164
|
+
cursor: ${hoverParams ? 'pointer' : 'default'};
|
|
166
165
|
display: ${hidden ? 'none' : 'block'};
|
|
167
166
|
height: fit-content;
|
|
168
167
|
}
|
|
@@ -180,8 +179,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
|
|
|
180
179
|
-webkit-transform: translate3d(0, 0, 0);
|
|
181
180
|
transform: translate3d(0, 0, 0);
|
|
182
181
|
pointer-events: none;
|
|
183
|
-
|
|
184
|
-
top: ${isScreenBasedBottom ? 'unset' : getItemTopStyle(area.top, area.anchorSide)};
|
|
182
|
+
top: ${getItemTopStyle(area.top, area.anchorSide)};
|
|
185
183
|
left: ${area.left * 100}vw;
|
|
186
184
|
transition: ${getTransitions(['left', 'top'], hoverParams)};
|
|
187
185
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { FC, useContext, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { ScrollPlaybackVideoManager } from '../utils/PlaybackVideoConverter/ScrollPlaybackVideoManager';
|
|
3
2
|
import { rangeMap } from '../utils/rangeMap';
|
|
4
3
|
import { ArticleRectContext } from '../provider/ArticleRectContext';
|
|
4
|
+
import { ScrollPlaybackVideoManager } from '@cntrl-site/sdk';
|
|
5
5
|
|
|
6
6
|
type PlaybackParams = { from: number, to: number };
|
|
7
7
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnchorSide, ItemAny
|
|
1
|
+
import { AnchorSide, ItemAny } from '@cntrl-site/sdk';
|
|
2
2
|
import { useKeyframeValue } from '../common/useKeyframeValue';
|
|
3
3
|
import { getItemTopStyle } from '../utils/getItemTopStyle';
|
|
4
4
|
import { useLayoutContext } from './useLayoutContext';
|
|
@@ -16,12 +16,8 @@ export const useItemPosition = (item: ItemAny, sectionId: string) => {
|
|
|
16
16
|
[layoutId]
|
|
17
17
|
);
|
|
18
18
|
const anchorSide = layoutId ? item.area[layoutId].anchorSide : AnchorSide.Top;
|
|
19
|
-
const positionType = layoutId ? item.area[layoutId].positionType : PositionType.ScreenBased;
|
|
20
|
-
// tp prevent fixed item (with anchor point bottom) to jump when scroll in safari on mobile
|
|
21
|
-
const isScreenBasedBottom = positionType === PositionType.ScreenBased && anchorSide === AnchorSide.Bottom;
|
|
22
19
|
return {
|
|
23
|
-
|
|
24
|
-
top: isScreenBasedBottom ? 'unset' : getItemTopStyle(top, anchorSide),
|
|
20
|
+
top: getItemTopStyle(top, anchorSide),
|
|
25
21
|
left: `${left * 100}vw`
|
|
26
22
|
};
|
|
27
23
|
};
|
|
@@ -199,16 +199,15 @@ export class RichTextConverter {
|
|
|
199
199
|
return styleGroups;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
private groupEntities(
|
|
202
|
+
private groupEntities(entitiesList: RichTextEntity[], styleGroups?: StyleGroup[]): EntitiesGroup[] | undefined {
|
|
203
203
|
const entitiesGroups: EntitiesGroup[] = [];
|
|
204
|
+
// some entities may have no data, need to filter them out (case with deleting a section/page that was linked to)
|
|
205
|
+
const entities = entitiesList.filter(e => e.hasOwnProperty('data'));
|
|
204
206
|
if (!entities.length && !styleGroups) return;
|
|
205
207
|
if (!styleGroups || styleGroups.length === 0) {
|
|
206
|
-
const dividersSet = entities.reduce((ds,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!e.hasOwnProperty('data')) return ds;
|
|
210
|
-
ds.add(e.start);
|
|
211
|
-
ds.add(e.end);
|
|
208
|
+
const dividersSet = entities.reduce((ds, s) => {
|
|
209
|
+
ds.add(s.start);
|
|
210
|
+
ds.add(s.end);
|
|
212
211
|
return ds;
|
|
213
212
|
}, new Set<number>([entities[0].start, entities[entities.length - 1].end]));
|
|
214
213
|
const dividers = Array.from(dividersSet).sort((a, b) => a - b);
|
|
@@ -235,10 +234,9 @@ export class RichTextConverter {
|
|
|
235
234
|
}
|
|
236
235
|
const start = entities[0].start < styleGroups[0].start ? entities[0].start : styleGroups[0].start;
|
|
237
236
|
const end = entities[entities.length - 1].end > styleGroups[styleGroups.length - 1].end ? entities[entities.length - 1].end : styleGroups[styleGroups.length - 1].end;
|
|
238
|
-
const entitiesDividers = entities.reduce((ds,
|
|
239
|
-
|
|
240
|
-
ds.add(
|
|
241
|
-
ds.add(e.end);
|
|
237
|
+
const entitiesDividers = entities.reduce((ds, s) => {
|
|
238
|
+
ds.add(s.start);
|
|
239
|
+
ds.add(s.end);
|
|
242
240
|
return ds;
|
|
243
241
|
}, new Set<number>([start, end]));
|
|
244
242
|
const entityDividers = Array.from(entitiesDividers).sort((a, b) => a - b);
|
|
@@ -251,7 +249,7 @@ export class RichTextConverter {
|
|
|
251
249
|
stylesGroup: styleGroups.filter(s => s.start >= start && s.end <= end),
|
|
252
250
|
start,
|
|
253
251
|
end,
|
|
254
|
-
...(entity && { link: entity.data
|
|
252
|
+
...(entity && { link: entity.data.url, target: entity.data.target })
|
|
255
253
|
});
|
|
256
254
|
}
|
|
257
255
|
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<component name="InspectionProjectProfileManager">
|
|
2
|
-
<profile version="1.0">
|
|
3
|
-
<option name="myName" value="Project Default" />
|
|
4
|
-
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
|
5
|
-
<option name="myValues">
|
|
6
|
-
<value>
|
|
7
|
-
<list size="1">
|
|
8
|
-
<item index="0" class="java.lang.String" itemvalue="jsx" />
|
|
9
|
-
</list>
|
|
10
|
-
</value>
|
|
11
|
-
</option>
|
|
12
|
-
<option name="myCustomValuesEnabled" value="true" />
|
|
13
|
-
</inspection_tool>
|
|
14
|
-
</profile>
|
|
15
|
-
</component>
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ScrollPlaybackVideoManager = void 0;
|
|
7
|
-
const ua_parser_js_1 = __importDefault(require("ua-parser-js"));
|
|
8
|
-
const VideoDecoder_1 = __importDefault(require("./VideoDecoder"));
|
|
9
|
-
class ScrollPlaybackVideoManager {
|
|
10
|
-
constructor(options) {
|
|
11
|
-
this.currentTime = 0;
|
|
12
|
-
this.targetTime = 0;
|
|
13
|
-
this.canvas = null;
|
|
14
|
-
this.context = null;
|
|
15
|
-
this.frames = [];
|
|
16
|
-
this.frameRate = 0;
|
|
17
|
-
this.transitioning = false;
|
|
18
|
-
this.debug = false;
|
|
19
|
-
this.frameThreshold = 0;
|
|
20
|
-
this.transitionSpeed = 10;
|
|
21
|
-
this.useWebCodecs = true;
|
|
22
|
-
this.resize = () => {
|
|
23
|
-
if (this.debug)
|
|
24
|
-
console.info('ScrollVideo resizing...');
|
|
25
|
-
if (this.canvas) {
|
|
26
|
-
this.setCoverStyle(this.canvas);
|
|
27
|
-
}
|
|
28
|
-
else if (this.video) {
|
|
29
|
-
this.setCoverStyle(this.video);
|
|
30
|
-
}
|
|
31
|
-
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
32
|
-
};
|
|
33
|
-
this.decodeVideo = () => {
|
|
34
|
-
if (!this.video)
|
|
35
|
-
return;
|
|
36
|
-
if (this.useWebCodecs && this.video.src) {
|
|
37
|
-
(0, VideoDecoder_1.default)(this.video.src, (frame) => {
|
|
38
|
-
this.frames.push(frame);
|
|
39
|
-
}, this.debug).then(() => {
|
|
40
|
-
if (!this.video || !this.container)
|
|
41
|
-
return;
|
|
42
|
-
if (this.frames.length === 0) {
|
|
43
|
-
if (this.debug)
|
|
44
|
-
console.error('No frames were received from webCodecs');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
this.frameRate = this.frames.length / this.video.duration;
|
|
48
|
-
if (this.debug)
|
|
49
|
-
console.info('Received', this.frames.length, 'frames');
|
|
50
|
-
this.canvas = document.createElement('canvas');
|
|
51
|
-
this.context = this.canvas.getContext('2d');
|
|
52
|
-
this.video.style.display = 'none';
|
|
53
|
-
this.container.appendChild(this.canvas);
|
|
54
|
-
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
55
|
-
}).catch(() => {
|
|
56
|
-
if (this.debug)
|
|
57
|
-
console.error('Error encountered while decoding video');
|
|
58
|
-
this.frames = [];
|
|
59
|
-
this.video.load();
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
64
|
-
this.resize();
|
|
65
|
-
});
|
|
66
|
-
const { src, videoContainer } = options;
|
|
67
|
-
if (typeof document !== 'object') {
|
|
68
|
-
console.error('ScrollVideo must be initiated in a DOM context');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (!videoContainer) {
|
|
72
|
-
console.error('scrollVideoContainer must be a valid DOM object');
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (!src) {
|
|
76
|
-
console.error('Must provide valid video src to ScrollVideo');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
this.container = typeof videoContainer === 'string' ? document.getElementById(videoContainer) : videoContainer;
|
|
80
|
-
this.resizeObserver.observe(this.container);
|
|
81
|
-
this.video = document.createElement('video');
|
|
82
|
-
this.video.src = src;
|
|
83
|
-
this.video.preload = 'auto';
|
|
84
|
-
this.video.tabIndex = 0;
|
|
85
|
-
this.video.playsInline = true;
|
|
86
|
-
this.video.muted = true;
|
|
87
|
-
this.video.pause();
|
|
88
|
-
this.video.load();
|
|
89
|
-
this.container.appendChild(this.video);
|
|
90
|
-
const browserEngine = new ua_parser_js_1.default().getEngine();
|
|
91
|
-
this.isSafari = browserEngine.name === 'WebKit';
|
|
92
|
-
if (this.debug && this.isSafari)
|
|
93
|
-
console.info('Safari browser detected');
|
|
94
|
-
this.video.addEventListener('loadedmetadata', () => this.setTargetTimePercent(0, true), { once: true });
|
|
95
|
-
this.video.addEventListener('progress', this.resize);
|
|
96
|
-
this.decodeVideo();
|
|
97
|
-
}
|
|
98
|
-
setCoverStyle(el) {
|
|
99
|
-
if (el && this.container) {
|
|
100
|
-
el.style.position = 'absolute';
|
|
101
|
-
el.style.top = '50%';
|
|
102
|
-
el.style.left = '50%';
|
|
103
|
-
el.style.transform = 'translate(-50%, -50%)';
|
|
104
|
-
const { width: containerWidth, height: containerHeight } = this.container.getBoundingClientRect();
|
|
105
|
-
const width = el.videoWidth || el.width;
|
|
106
|
-
const height = el.videoHeight || el.height;
|
|
107
|
-
if (containerWidth / containerHeight > width / height) {
|
|
108
|
-
el.style.width = '100%';
|
|
109
|
-
el.style.height = 'auto';
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
el.style.height = '100%';
|
|
113
|
-
el.style.width = 'auto';
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
paintCanvasFrame(frameNum) {
|
|
118
|
-
if (this.canvas) {
|
|
119
|
-
const frameIdx = Math.min(frameNum, this.frames.length - 1);
|
|
120
|
-
const currFrame = this.frames[frameIdx];
|
|
121
|
-
if (currFrame && this.container) {
|
|
122
|
-
if (this.debug)
|
|
123
|
-
console.info('Painting frame', frameIdx);
|
|
124
|
-
this.canvas.width = currFrame.width;
|
|
125
|
-
this.canvas.height = currFrame.height;
|
|
126
|
-
const { width, height } = this.container.getBoundingClientRect();
|
|
127
|
-
this.resetCanvasDimensions(width, height, currFrame.width, currFrame.height);
|
|
128
|
-
this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
transitionToTargetTime(jump) {
|
|
133
|
-
if (!this.video)
|
|
134
|
-
return;
|
|
135
|
-
if (this.debug)
|
|
136
|
-
console.info('Transitioning targetTime:', this.targetTime, 'currentTime:', this.currentTime);
|
|
137
|
-
if (isNaN(this.targetTime) || Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) {
|
|
138
|
-
this.video.pause();
|
|
139
|
-
this.transitioning = false;
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
// Make sure we don't go out of time bounds
|
|
143
|
-
if (this.targetTime > this.video.duration) {
|
|
144
|
-
this.targetTime = this.video.duration;
|
|
145
|
-
}
|
|
146
|
-
if (this.targetTime < 0) {
|
|
147
|
-
this.targetTime = 0;
|
|
148
|
-
}
|
|
149
|
-
// How far forward we need to transition
|
|
150
|
-
const transitionForward = this.targetTime - this.currentTime;
|
|
151
|
-
if (this.canvas) {
|
|
152
|
-
// Update currentTime and paint the closest frame
|
|
153
|
-
this.currentTime += transitionForward / (256 / this.transitionSpeed);
|
|
154
|
-
// If jump, we go directly to the frame
|
|
155
|
-
if (jump) {
|
|
156
|
-
this.currentTime = this.targetTime;
|
|
157
|
-
}
|
|
158
|
-
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
159
|
-
}
|
|
160
|
-
else if (jump || this.isSafari || this.targetTime - this.currentTime < 0) {
|
|
161
|
-
this.video.pause();
|
|
162
|
-
this.currentTime += transitionForward / (64 / this.transitionSpeed);
|
|
163
|
-
// If jump, we go directly to the frame
|
|
164
|
-
if (jump) {
|
|
165
|
-
this.currentTime = this.targetTime;
|
|
166
|
-
}
|
|
167
|
-
this.video.currentTime = this.currentTime;
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
// Otherwise, we play the video and adjust the playbackRate to get a smoother
|
|
171
|
-
// animation effect.
|
|
172
|
-
const playbackRate = Math.max(Math.min(transitionForward * 4, this.transitionSpeed, 16), 1);
|
|
173
|
-
if (this.debug)
|
|
174
|
-
console.info('ScrollVideo playbackRate:', playbackRate);
|
|
175
|
-
if (!isNaN(playbackRate)) {
|
|
176
|
-
this.video.playbackRate = playbackRate;
|
|
177
|
-
this.video.play();
|
|
178
|
-
}
|
|
179
|
-
this.currentTime = this.video.currentTime;
|
|
180
|
-
}
|
|
181
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
182
|
-
requestAnimationFrame(() => this.transitionToTargetTime(jump));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
resetCanvasDimensions(w, h, frameW, frameH) {
|
|
186
|
-
if (!this.canvas)
|
|
187
|
-
return;
|
|
188
|
-
if (w / h > frameW / frameH) {
|
|
189
|
-
this.canvas.style.width = '100%';
|
|
190
|
-
this.canvas.style.height = 'auto';
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
this.canvas.style.height = '100%';
|
|
194
|
-
this.canvas.style.width = 'auto';
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
setTargetTimePercent(setPercentage, jump = true) {
|
|
198
|
-
if (!this.video)
|
|
199
|
-
return;
|
|
200
|
-
this.targetTime = Math.max(Math.min(setPercentage, 1), 0)
|
|
201
|
-
* (this.frames.length && this.frameRate ? this.frames.length / this.frameRate : this.video.duration);
|
|
202
|
-
if (!jump && Math.abs(this.currentTime - this.targetTime) < this.frameThreshold)
|
|
203
|
-
return;
|
|
204
|
-
if (!jump && this.transitioning)
|
|
205
|
-
return;
|
|
206
|
-
if (!this.canvas && !this.video.paused)
|
|
207
|
-
this.video.play();
|
|
208
|
-
this.transitioning = true;
|
|
209
|
-
this.transitionToTargetTime(jump);
|
|
210
|
-
}
|
|
211
|
-
destroy() {
|
|
212
|
-
var _a;
|
|
213
|
-
this.resizeObserver.unobserve(this.container);
|
|
214
|
-
(_a = this.video) === null || _a === void 0 ? void 0 : _a.removeEventListener('progress', this.resize);
|
|
215
|
-
if (this.debug)
|
|
216
|
-
console.info('Destroying ScrollVideo');
|
|
217
|
-
if (this.container)
|
|
218
|
-
this.container.innerHTML = '';
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
exports.ScrollPlaybackVideoManager = ScrollPlaybackVideoManager;
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.Writer = void 0;
|
|
27
|
-
// @ts-ignore
|
|
28
|
-
const MP4Box = __importStar(require("mp4box"));
|
|
29
|
-
class Writer {
|
|
30
|
-
constructor(size) {
|
|
31
|
-
this.data = new Uint8Array(size);
|
|
32
|
-
this.idx = 0;
|
|
33
|
-
this.size = size;
|
|
34
|
-
}
|
|
35
|
-
getData() {
|
|
36
|
-
if (this.idx !== this.size)
|
|
37
|
-
throw new Error('Mismatch between size reserved and sized used');
|
|
38
|
-
return this.data.slice(0, this.idx);
|
|
39
|
-
}
|
|
40
|
-
writeUint8(value) {
|
|
41
|
-
this.data.set([value], this.idx);
|
|
42
|
-
this.idx += 1;
|
|
43
|
-
}
|
|
44
|
-
writeUint16(value) {
|
|
45
|
-
const arr = new Uint16Array(1);
|
|
46
|
-
arr[0] = value;
|
|
47
|
-
const buffer = new Uint8Array(arr.buffer);
|
|
48
|
-
this.data.set([buffer[1], buffer[0]], this.idx);
|
|
49
|
-
this.idx += 2;
|
|
50
|
-
}
|
|
51
|
-
writeUint8Array(value) {
|
|
52
|
-
this.data.set(value, this.idx);
|
|
53
|
-
this.idx += value.length;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
exports.Writer = Writer;
|
|
57
|
-
const getExtradata = (avccBox) => {
|
|
58
|
-
let i;
|
|
59
|
-
let size = 7;
|
|
60
|
-
for (i = 0; i < avccBox.SPS.length; i += 1) {
|
|
61
|
-
// nalu length is encoded as a uint16.
|
|
62
|
-
size += 2 + avccBox.SPS[i].length;
|
|
63
|
-
}
|
|
64
|
-
for (i = 0; i < avccBox.PPS.length; i += 1) {
|
|
65
|
-
// nalu length is encoded as a uint16.
|
|
66
|
-
size += 2 + avccBox.PPS[i].length;
|
|
67
|
-
}
|
|
68
|
-
const writer = new Writer(size);
|
|
69
|
-
writer.writeUint8(avccBox.configurationVersion);
|
|
70
|
-
writer.writeUint8(avccBox.AVCProfileIndication);
|
|
71
|
-
writer.writeUint8(avccBox.profile_compatibility);
|
|
72
|
-
writer.writeUint8(avccBox.AVCLevelIndication);
|
|
73
|
-
writer.writeUint8(avccBox.lengthSizeMinusOne + (63 << 2));
|
|
74
|
-
writer.writeUint8(avccBox.nb_SPS_nalus + (7 << 5));
|
|
75
|
-
for (i = 0; i < avccBox.SPS.length; i += 1) {
|
|
76
|
-
writer.writeUint16(avccBox.SPS[i].length);
|
|
77
|
-
writer.writeUint8Array(avccBox.SPS[i].nalu);
|
|
78
|
-
}
|
|
79
|
-
writer.writeUint8(avccBox.nb_PPS_nalus);
|
|
80
|
-
for (i = 0; i < avccBox.PPS.length; i += 1) {
|
|
81
|
-
writer.writeUint16(avccBox.PPS[i].length);
|
|
82
|
-
writer.writeUint8Array(avccBox.PPS[i].nalu);
|
|
83
|
-
}
|
|
84
|
-
return writer.getData();
|
|
85
|
-
};
|
|
86
|
-
const decodeVideo = (src, emitFrame, { VideoDecoder, EncodedVideoChunk, debug }) => new Promise((resolve, reject) => {
|
|
87
|
-
if (debug)
|
|
88
|
-
console.info('Decoding video from', src);
|
|
89
|
-
try {
|
|
90
|
-
const mp4boxfile = MP4Box.createFile();
|
|
91
|
-
let codec;
|
|
92
|
-
const decoder = new VideoDecoder({
|
|
93
|
-
output: (frame) => {
|
|
94
|
-
console.log(frame);
|
|
95
|
-
createImageBitmap(frame, { resizeQuality: 'low' }).then((bitmap) => {
|
|
96
|
-
emitFrame(bitmap);
|
|
97
|
-
frame.close();
|
|
98
|
-
if (decoder.decodeQueueSize <= 0) {
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
if (decoder.state !== 'closed') {
|
|
101
|
-
decoder.close();
|
|
102
|
-
resolve();
|
|
103
|
-
}
|
|
104
|
-
}, 500);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
},
|
|
108
|
-
error: (e) => {
|
|
109
|
-
console.error(e);
|
|
110
|
-
reject(e);
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
mp4boxfile.onReady = (info) => {
|
|
114
|
-
if (info && info.videoTracks && info.videoTracks[0]) {
|
|
115
|
-
[{ codec }] = info.videoTracks;
|
|
116
|
-
if (debug)
|
|
117
|
-
console.info('Video with codec:', codec);
|
|
118
|
-
const avccBox = mp4boxfile.moov.traks[0].mdia.minf.stbl.stsd.entries[0].avcC;
|
|
119
|
-
const extradata = getExtradata(avccBox);
|
|
120
|
-
decoder.configure({ codec, description: extradata });
|
|
121
|
-
mp4boxfile.setExtractionOptions(info.videoTracks[0].id);
|
|
122
|
-
mp4boxfile.start();
|
|
123
|
-
}
|
|
124
|
-
else
|
|
125
|
-
reject(new Error('URL provided is not a valid mp4 video file.'));
|
|
126
|
-
};
|
|
127
|
-
mp4boxfile.onSamples = (track_id, ref, samples) => {
|
|
128
|
-
for (let i = 0; i < samples.length; i += 1) {
|
|
129
|
-
const sample = samples[i];
|
|
130
|
-
const type = sample.is_sync ? 'key' : 'delta';
|
|
131
|
-
const chunk = new EncodedVideoChunk({
|
|
132
|
-
type,
|
|
133
|
-
timestamp: sample.cts,
|
|
134
|
-
duration: sample.duration,
|
|
135
|
-
data: sample.data,
|
|
136
|
-
});
|
|
137
|
-
decoder.decode(chunk);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
fetch(src).then((res) => {
|
|
141
|
-
const reader = res.body.getReader();
|
|
142
|
-
let offset = 0;
|
|
143
|
-
//@ts-ignore
|
|
144
|
-
function appendBuffers({ done, value }) {
|
|
145
|
-
if (done) {
|
|
146
|
-
mp4boxfile.flush();
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
const buf = value.buffer;
|
|
150
|
-
buf.fileStart = offset;
|
|
151
|
-
offset += buf.byteLength;
|
|
152
|
-
mp4boxfile.appendBuffer(buf);
|
|
153
|
-
return reader.read().then(appendBuffers);
|
|
154
|
-
}
|
|
155
|
-
return reader.read().then(appendBuffers);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch (e) {
|
|
159
|
-
reject(e);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
exports.default = (src, emitFrame, debug) => {
|
|
163
|
-
if (typeof VideoDecoder === 'function' && typeof EncodedVideoChunk === 'function') {
|
|
164
|
-
if (debug)
|
|
165
|
-
console.info('WebCodecs is natively supported, using native version...');
|
|
166
|
-
return decodeVideo(src, emitFrame, {
|
|
167
|
-
VideoDecoder,
|
|
168
|
-
EncodedVideoChunk,
|
|
169
|
-
debug,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
if (debug)
|
|
173
|
-
console.info('WebCodecs is not available in this browser.');
|
|
174
|
-
return Promise.resolve();
|
|
175
|
-
};
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import UAParser from 'ua-parser-js';
|
|
2
|
-
import videoDecoder from './VideoDecoder';
|
|
3
|
-
|
|
4
|
-
interface ScrollVideoOptions {
|
|
5
|
-
src: string;
|
|
6
|
-
videoContainer: HTMLElement | string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class ScrollPlaybackVideoManager {
|
|
10
|
-
private container?: HTMLElement;
|
|
11
|
-
private video?: HTMLVideoElement;
|
|
12
|
-
private isSafari?: boolean;
|
|
13
|
-
private currentTime = 0;
|
|
14
|
-
private targetTime = 0;
|
|
15
|
-
private canvas: HTMLCanvasElement | null = null;
|
|
16
|
-
private context: CanvasRenderingContext2D | null = null;
|
|
17
|
-
private frames: ImageBitmap[] = [];
|
|
18
|
-
private frameRate = 0;
|
|
19
|
-
private transitioning = false;
|
|
20
|
-
private debug: boolean = false;
|
|
21
|
-
private frameThreshold: number = 0;
|
|
22
|
-
private transitionSpeed: number = 10;
|
|
23
|
-
private useWebCodecs: boolean = true;
|
|
24
|
-
private resizeObserver: ResizeObserver;
|
|
25
|
-
|
|
26
|
-
constructor(options: ScrollVideoOptions) {
|
|
27
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
28
|
-
this.resize();
|
|
29
|
-
});
|
|
30
|
-
const {
|
|
31
|
-
src,
|
|
32
|
-
videoContainer
|
|
33
|
-
} = options;
|
|
34
|
-
if (typeof document !== 'object') {
|
|
35
|
-
console.error('ScrollVideo must be initiated in a DOM context');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if (!videoContainer) {
|
|
39
|
-
console.error('scrollVideoContainer must be a valid DOM object');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (!src) {
|
|
43
|
-
console.error('Must provide valid video src to ScrollVideo');
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
this.container = typeof videoContainer === 'string' ? document.getElementById(videoContainer)! : videoContainer;
|
|
48
|
-
this.resizeObserver.observe(this.container);
|
|
49
|
-
this.video = document.createElement('video');
|
|
50
|
-
this.video.src = src;
|
|
51
|
-
this.video.preload = 'auto';
|
|
52
|
-
this.video.tabIndex = 0;
|
|
53
|
-
this.video.playsInline = true;
|
|
54
|
-
this.video.muted = true;
|
|
55
|
-
this.video.pause();
|
|
56
|
-
this.video.load();
|
|
57
|
-
this.container.appendChild(this.video);
|
|
58
|
-
const browserEngine = new UAParser().getEngine();
|
|
59
|
-
this.isSafari = browserEngine.name === 'WebKit';
|
|
60
|
-
if (this.debug && this.isSafari) console.info('Safari browser detected');
|
|
61
|
-
this.video.addEventListener('loadedmetadata', () => this.setTargetTimePercent(0, true), { once: true });
|
|
62
|
-
this.video.addEventListener('progress', this.resize);
|
|
63
|
-
this.decodeVideo();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private setCoverStyle(el: any) {
|
|
67
|
-
if (el && this.container) {
|
|
68
|
-
el.style.position = 'absolute';
|
|
69
|
-
el.style.top = '50%';
|
|
70
|
-
el.style.left = '50%';
|
|
71
|
-
el.style.transform = 'translate(-50%, -50%)';
|
|
72
|
-
const { width: containerWidth, height: containerHeight } = this.container.getBoundingClientRect();
|
|
73
|
-
const width = el.videoWidth || el.width;
|
|
74
|
-
const height = el.videoHeight || el.height;
|
|
75
|
-
if (containerWidth / containerHeight > width / height) {
|
|
76
|
-
el.style.width = '100%';
|
|
77
|
-
el.style.height = 'auto';
|
|
78
|
-
} else {
|
|
79
|
-
el.style.height = '100%';
|
|
80
|
-
el.style.width = 'auto';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private resize = () => {
|
|
86
|
-
if (this.debug) console.info('ScrollVideo resizing...');
|
|
87
|
-
if (this.canvas) {
|
|
88
|
-
this.setCoverStyle(this.canvas);
|
|
89
|
-
} else if (this.video) {
|
|
90
|
-
this.setCoverStyle(this.video);
|
|
91
|
-
}
|
|
92
|
-
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
private decodeVideo = () => {
|
|
96
|
-
if (!this.video) return;
|
|
97
|
-
if (this.useWebCodecs && this.video.src) {
|
|
98
|
-
videoDecoder(this.video.src, (frame: ImageBitmap) => {
|
|
99
|
-
this.frames.push(frame);
|
|
100
|
-
},
|
|
101
|
-
this.debug,
|
|
102
|
-
).then(() => {
|
|
103
|
-
if (!this.video || !this.container) return
|
|
104
|
-
if (this.frames.length === 0) {
|
|
105
|
-
if (this.debug) console.error('No frames were received from webCodecs');
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
this.frameRate = this.frames.length / this.video.duration;
|
|
109
|
-
if (this.debug) console.info('Received', this.frames.length, 'frames');
|
|
110
|
-
this.canvas = document.createElement('canvas');
|
|
111
|
-
this.context = this.canvas.getContext('2d')!;
|
|
112
|
-
this.video.style.display = 'none';
|
|
113
|
-
this.container.appendChild(this.canvas);
|
|
114
|
-
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
115
|
-
}).catch(() => {
|
|
116
|
-
if (this.debug) console.error('Error encountered while decoding video');
|
|
117
|
-
this.frames = [];
|
|
118
|
-
this.video!.load();
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private paintCanvasFrame(frameNum: number) {
|
|
124
|
-
if (this.canvas) {
|
|
125
|
-
const frameIdx = Math.min(frameNum, this.frames.length - 1);
|
|
126
|
-
const currFrame = this.frames[frameIdx];
|
|
127
|
-
if (currFrame && this.container) {
|
|
128
|
-
if (this.debug) console.info('Painting frame', frameIdx);
|
|
129
|
-
this.canvas.width = currFrame.width;
|
|
130
|
-
this.canvas.height = currFrame.height;
|
|
131
|
-
const { width, height } = this.container.getBoundingClientRect();
|
|
132
|
-
this.resetCanvasDimensions(width, height, currFrame.width, currFrame.height);
|
|
133
|
-
this.context!.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private transitionToTargetTime(jump: boolean) {
|
|
139
|
-
if (!this.video) return;
|
|
140
|
-
if (this.debug) console.info('Transitioning targetTime:', this.targetTime, 'currentTime:', this.currentTime);
|
|
141
|
-
if (isNaN(this.targetTime) || Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) {
|
|
142
|
-
this.video.pause();
|
|
143
|
-
this.transitioning = false;
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
// Make sure we don't go out of time bounds
|
|
147
|
-
if (this.targetTime > this.video.duration) {
|
|
148
|
-
this.targetTime = this.video.duration;
|
|
149
|
-
}
|
|
150
|
-
if (this.targetTime < 0) {
|
|
151
|
-
this.targetTime = 0;
|
|
152
|
-
}
|
|
153
|
-
// How far forward we need to transition
|
|
154
|
-
const transitionForward = this.targetTime - this.currentTime;
|
|
155
|
-
if (this.canvas) {
|
|
156
|
-
// Update currentTime and paint the closest frame
|
|
157
|
-
this.currentTime += transitionForward / (256 / this.transitionSpeed);
|
|
158
|
-
// If jump, we go directly to the frame
|
|
159
|
-
if (jump) { this.currentTime = this.targetTime }
|
|
160
|
-
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
161
|
-
} else if (jump || this.isSafari || this.targetTime - this.currentTime < 0) {
|
|
162
|
-
this.video.pause();
|
|
163
|
-
this.currentTime += transitionForward / (64 / this.transitionSpeed);
|
|
164
|
-
// If jump, we go directly to the frame
|
|
165
|
-
if (jump) { this.currentTime = this.targetTime;}
|
|
166
|
-
this.video.currentTime = this.currentTime;
|
|
167
|
-
} else {
|
|
168
|
-
// Otherwise, we play the video and adjust the playbackRate to get a smoother
|
|
169
|
-
// animation effect.
|
|
170
|
-
const playbackRate = Math.max(Math.min(transitionForward * 4, this.transitionSpeed, 16), 1);
|
|
171
|
-
if (this.debug) console.info('ScrollVideo playbackRate:', playbackRate);
|
|
172
|
-
if (!isNaN(playbackRate)) {
|
|
173
|
-
this.video.playbackRate = playbackRate;
|
|
174
|
-
this.video.play();
|
|
175
|
-
}
|
|
176
|
-
this.currentTime = this.video.currentTime;
|
|
177
|
-
}
|
|
178
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
179
|
-
requestAnimationFrame(() => this.transitionToTargetTime(jump));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private resetCanvasDimensions(w: number, h: number, frameW: number, frameH: number) {
|
|
184
|
-
if (!this.canvas) return;
|
|
185
|
-
if (w / h > frameW / frameH) {
|
|
186
|
-
this.canvas.style.width = '100%';
|
|
187
|
-
this.canvas.style.height = 'auto';
|
|
188
|
-
} else {
|
|
189
|
-
this.canvas.style.height = '100%';
|
|
190
|
-
this.canvas.style.width = 'auto';
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
setTargetTimePercent(setPercentage: number, jump: boolean = true): void {
|
|
195
|
-
if (!this.video) return;
|
|
196
|
-
this.targetTime = Math.max(Math.min(setPercentage, 1), 0)
|
|
197
|
-
* (this.frames.length && this.frameRate ? this.frames.length / this.frameRate : this.video.duration);
|
|
198
|
-
if (!jump && Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) return;
|
|
199
|
-
if (!jump && this.transitioning) return;
|
|
200
|
-
if (!this.canvas && !this.video.paused) this.video.play();
|
|
201
|
-
this.transitioning = true;
|
|
202
|
-
this.transitionToTargetTime(jump);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
destroy(): void {
|
|
206
|
-
this.resizeObserver.unobserve(this.container!);
|
|
207
|
-
this.video?.removeEventListener('progress', this.resize);
|
|
208
|
-
if (this.debug) console.info('Destroying ScrollVideo');
|
|
209
|
-
if (this.container) this.container.innerHTML = '';
|
|
210
|
-
}
|
|
211
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// @ts-ignore
|
|
2
|
-
import * as MP4Box from 'mp4box';
|
|
3
|
-
|
|
4
|
-
export class Writer {
|
|
5
|
-
private data: Uint8Array;
|
|
6
|
-
private idx: number;
|
|
7
|
-
private size: number;
|
|
8
|
-
|
|
9
|
-
constructor(size: number) {
|
|
10
|
-
this.data = new Uint8Array(size);
|
|
11
|
-
this.idx = 0;
|
|
12
|
-
this.size = size;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
getData() {
|
|
16
|
-
if (this.idx !== this.size) throw new Error('Mismatch between size reserved and sized used');
|
|
17
|
-
return this.data.slice(0, this.idx);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
writeUint8(value: number) {
|
|
21
|
-
this.data.set([value], this.idx);
|
|
22
|
-
this.idx += 1;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
writeUint16(value: number) {
|
|
26
|
-
const arr = new Uint16Array(1);
|
|
27
|
-
arr[0] = value;
|
|
28
|
-
const buffer = new Uint8Array(arr.buffer);
|
|
29
|
-
this.data.set([buffer[1], buffer[0]], this.idx);
|
|
30
|
-
this.idx += 2;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
writeUint8Array(value: Uint8Array) {
|
|
34
|
-
this.data.set(value, this.idx);
|
|
35
|
-
this.idx += value.length;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface AVCCBox {
|
|
40
|
-
configurationVersion: number;
|
|
41
|
-
AVCProfileIndication: number;
|
|
42
|
-
profile_compatibility: number;
|
|
43
|
-
AVCLevelIndication: number;
|
|
44
|
-
lengthSizeMinusOne: number;
|
|
45
|
-
nb_SPS_nalus: number;
|
|
46
|
-
SPS: { length: number; nalu: Uint8Array }[];
|
|
47
|
-
nb_PPS_nalus: number;
|
|
48
|
-
PPS: { length: number; nalu: Uint8Array }[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const getExtradata = (avccBox: AVCCBox) => {
|
|
52
|
-
let i;
|
|
53
|
-
let size = 7;
|
|
54
|
-
for (i = 0; i < avccBox.SPS.length; i += 1) {
|
|
55
|
-
// nalu length is encoded as a uint16.
|
|
56
|
-
size += 2 + avccBox.SPS[i].length;
|
|
57
|
-
}
|
|
58
|
-
for (i = 0; i < avccBox.PPS.length; i += 1) {
|
|
59
|
-
// nalu length is encoded as a uint16.
|
|
60
|
-
size += 2 + avccBox.PPS[i].length;
|
|
61
|
-
}
|
|
62
|
-
const writer = new Writer(size);
|
|
63
|
-
writer.writeUint8(avccBox.configurationVersion);
|
|
64
|
-
writer.writeUint8(avccBox.AVCProfileIndication);
|
|
65
|
-
writer.writeUint8(avccBox.profile_compatibility);
|
|
66
|
-
writer.writeUint8(avccBox.AVCLevelIndication);
|
|
67
|
-
writer.writeUint8(avccBox.lengthSizeMinusOne + (63 << 2));
|
|
68
|
-
writer.writeUint8(avccBox.nb_SPS_nalus + (7 << 5));
|
|
69
|
-
for (i = 0; i < avccBox.SPS.length; i += 1) {
|
|
70
|
-
writer.writeUint16(avccBox.SPS[i].length);
|
|
71
|
-
writer.writeUint8Array(avccBox.SPS[i].nalu);
|
|
72
|
-
}
|
|
73
|
-
writer.writeUint8(avccBox.nb_PPS_nalus);
|
|
74
|
-
for (i = 0; i < avccBox.PPS.length; i += 1) {
|
|
75
|
-
writer.writeUint16(avccBox.PPS[i].length);
|
|
76
|
-
writer.writeUint8Array(avccBox.PPS[i].nalu);
|
|
77
|
-
}
|
|
78
|
-
return writer.getData();
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const decodeVideo = (
|
|
82
|
-
src: string,
|
|
83
|
-
emitFrame: (bitmap: ImageBitmap) => void,
|
|
84
|
-
{ VideoDecoder, EncodedVideoChunk, debug }: any
|
|
85
|
-
): Promise<void> =>
|
|
86
|
-
new Promise((resolve, reject) => {
|
|
87
|
-
if (debug) console.info('Decoding video from', src);
|
|
88
|
-
try {
|
|
89
|
-
const mp4boxfile = MP4Box.createFile();
|
|
90
|
-
let codec;
|
|
91
|
-
const decoder = new VideoDecoder({
|
|
92
|
-
output: (frame: VideoFrame) => {
|
|
93
|
-
console.log(frame);
|
|
94
|
-
createImageBitmap(frame, { resizeQuality: 'low' }).then((bitmap) => {
|
|
95
|
-
emitFrame(bitmap);
|
|
96
|
-
frame.close();
|
|
97
|
-
if (decoder.decodeQueueSize <= 0) {
|
|
98
|
-
setTimeout(() => {
|
|
99
|
-
if (decoder.state !== 'closed') {
|
|
100
|
-
decoder.close();
|
|
101
|
-
resolve();
|
|
102
|
-
}
|
|
103
|
-
}, 500);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
error: (e: any) => {
|
|
108
|
-
console.error(e);
|
|
109
|
-
reject(e);
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
mp4boxfile.onReady = (info: any) => {
|
|
114
|
-
if (info && info.videoTracks && info.videoTracks[0]) {
|
|
115
|
-
[{ codec }] = info.videoTracks;
|
|
116
|
-
if (debug) console.info('Video with codec:', codec);
|
|
117
|
-
const avccBox = mp4boxfile.moov.traks[0].mdia.minf.stbl.stsd.entries[0].avcC;
|
|
118
|
-
const extradata = getExtradata(avccBox);
|
|
119
|
-
decoder.configure({ codec, description: extradata });
|
|
120
|
-
mp4boxfile.setExtractionOptions(info.videoTracks[0].id);
|
|
121
|
-
mp4boxfile.start();
|
|
122
|
-
} else reject(new Error('URL provided is not a valid mp4 video file.'));
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
mp4boxfile.onSamples = (track_id: number, ref: any, samples: any[]) => {
|
|
126
|
-
for (let i = 0; i < samples.length; i += 1) {
|
|
127
|
-
const sample = samples[i];
|
|
128
|
-
const type = sample.is_sync ? 'key' : 'delta';
|
|
129
|
-
const chunk = new EncodedVideoChunk({
|
|
130
|
-
type,
|
|
131
|
-
timestamp: sample.cts,
|
|
132
|
-
duration: sample.duration,
|
|
133
|
-
data: sample.data,
|
|
134
|
-
});
|
|
135
|
-
decoder.decode(chunk);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
fetch(src).then((res) => {
|
|
140
|
-
const reader = res.body!.getReader();
|
|
141
|
-
let offset = 0;
|
|
142
|
-
//@ts-ignore
|
|
143
|
-
function appendBuffers({ done, value }: any) {
|
|
144
|
-
if (done) {
|
|
145
|
-
mp4boxfile.flush();
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const buf = value.buffer;
|
|
149
|
-
buf.fileStart = offset;
|
|
150
|
-
offset += buf.byteLength;
|
|
151
|
-
mp4boxfile.appendBuffer(buf);
|
|
152
|
-
return reader.read().then(appendBuffers);
|
|
153
|
-
}
|
|
154
|
-
return reader.read().then(appendBuffers);
|
|
155
|
-
});
|
|
156
|
-
} catch (e) {
|
|
157
|
-
reject(e);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
export default ( src: string, emitFrame: (bitmap: ImageBitmap) => void, debug: boolean): Promise<void> => {
|
|
163
|
-
if (typeof VideoDecoder === 'function' && typeof EncodedVideoChunk === 'function') {
|
|
164
|
-
if (debug)
|
|
165
|
-
console.info('WebCodecs is natively supported, using native version...');
|
|
166
|
-
return decodeVideo(src, emitFrame, {
|
|
167
|
-
VideoDecoder,
|
|
168
|
-
EncodedVideoChunk,
|
|
169
|
-
debug,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
if (debug) console.info('WebCodecs is not available in this browser.');
|
|
173
|
-
return Promise.resolve();
|
|
174
|
-
};
|