@cntrl-site/sdk 1.8.0 → 1.9.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/ScrollPlaybackVideoManager/ScrollPlaybackVideoManager.js +221 -0
- package/lib/VideoDecoder/VideoDecoder.js +175 -0
- package/lib/index.js +3 -1
- package/lib/types/article/ArticleItemType.js +1 -1
- package/lib/types/article/ItemArea.js +3 -3
- package/lib/types/article/RichText.js +4 -4
- package/lib/types/article/Section.js +1 -1
- package/lib/types/keyframe/Keyframe.js +1 -1
- package/lib/types/project/Fonts.js +1 -1
- package/package.json +5 -2
- package/src/Client/Client.test.ts +2 -2
- package/src/ScrollPlaybackVideoManager/ScrollPlaybackVideoManager.ts +212 -0
- package/src/VideoDecoder/VideoDecoder.ts +174 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
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/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;
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KeyframeType = exports.PositionType = exports.AnchorSide = exports.ScaleAnchor = exports.ArticleItemType = exports.VerticalAlign = exports.TextTransform = exports.TextDecoration = exports.TextAlign = exports.SectionHeightMode = exports.getLayoutMediaQuery = exports.getLayoutStyles = exports.FontFaceGenerator = exports.CntrlClient = void 0;
|
|
3
|
+
exports.KeyframeType = exports.PositionType = exports.AnchorSide = exports.ScaleAnchor = exports.ArticleItemType = exports.VerticalAlign = exports.TextTransform = exports.TextDecoration = exports.TextAlign = exports.SectionHeightMode = exports.ScrollPlaybackVideoManager = exports.getLayoutMediaQuery = exports.getLayoutStyles = exports.FontFaceGenerator = exports.CntrlClient = void 0;
|
|
4
4
|
// logic
|
|
5
5
|
var Client_1 = require("./Client/Client");
|
|
6
6
|
Object.defineProperty(exports, "CntrlClient", { enumerable: true, get: function () { return Client_1.Client; } });
|
|
@@ -9,6 +9,8 @@ Object.defineProperty(exports, "FontFaceGenerator", { enumerable: true, get: fun
|
|
|
9
9
|
var utils_1 = require("./utils");
|
|
10
10
|
Object.defineProperty(exports, "getLayoutStyles", { enumerable: true, get: function () { return utils_1.getLayoutStyles; } });
|
|
11
11
|
Object.defineProperty(exports, "getLayoutMediaQuery", { enumerable: true, get: function () { return utils_1.getLayoutMediaQuery; } });
|
|
12
|
+
var ScrollPlaybackVideoManager_1 = require("./ScrollPlaybackVideoManager/ScrollPlaybackVideoManager");
|
|
13
|
+
Object.defineProperty(exports, "ScrollPlaybackVideoManager", { enumerable: true, get: function () { return ScrollPlaybackVideoManager_1.ScrollPlaybackVideoManager; } });
|
|
12
14
|
// enums
|
|
13
15
|
var Section_1 = require("./types/article/Section");
|
|
14
16
|
Object.defineProperty(exports, "SectionHeightMode", { enumerable: true, get: function () { return Section_1.SectionHeightMode; } });
|
|
@@ -11,4 +11,4 @@ var ArticleItemType;
|
|
|
11
11
|
ArticleItemType["YoutubeEmbed"] = "youtube-embed";
|
|
12
12
|
ArticleItemType["Custom"] = "custom";
|
|
13
13
|
ArticleItemType["Group"] = "group";
|
|
14
|
-
})(ArticleItemType
|
|
14
|
+
})(ArticleItemType || (exports.ArticleItemType = ArticleItemType = {}));
|
|
@@ -6,12 +6,12 @@ var AnchorSide;
|
|
|
6
6
|
AnchorSide["Top"] = "top";
|
|
7
7
|
AnchorSide["Bottom"] = "bottom";
|
|
8
8
|
AnchorSide["Center"] = "center";
|
|
9
|
-
})(AnchorSide
|
|
9
|
+
})(AnchorSide || (exports.AnchorSide = AnchorSide = {}));
|
|
10
10
|
var PositionType;
|
|
11
11
|
(function (PositionType) {
|
|
12
12
|
PositionType["SectionBased"] = "section-based";
|
|
13
13
|
PositionType["ScreenBased"] = "screen-based";
|
|
14
|
-
})(PositionType
|
|
14
|
+
})(PositionType || (exports.PositionType = PositionType = {}));
|
|
15
15
|
var ScaleAnchor;
|
|
16
16
|
(function (ScaleAnchor) {
|
|
17
17
|
ScaleAnchor["TopLeft"] = "top-left";
|
|
@@ -23,4 +23,4 @@ var ScaleAnchor;
|
|
|
23
23
|
ScaleAnchor["BottomLeft"] = "bottom-left";
|
|
24
24
|
ScaleAnchor["BottomCenter"] = "bottom-center";
|
|
25
25
|
ScaleAnchor["BottomRight"] = "bottom-right";
|
|
26
|
-
})(ScaleAnchor
|
|
26
|
+
})(ScaleAnchor || (exports.ScaleAnchor = ScaleAnchor = {}));
|
|
@@ -7,21 +7,21 @@ var TextAlign;
|
|
|
7
7
|
TextAlign["Right"] = "right";
|
|
8
8
|
TextAlign["Center"] = "center";
|
|
9
9
|
TextAlign["Justify"] = "justify";
|
|
10
|
-
})(TextAlign
|
|
10
|
+
})(TextAlign || (exports.TextAlign = TextAlign = {}));
|
|
11
11
|
var TextTransform;
|
|
12
12
|
(function (TextTransform) {
|
|
13
13
|
TextTransform["None"] = "none";
|
|
14
14
|
TextTransform["Uppercase"] = "uppercase";
|
|
15
15
|
TextTransform["Lowercase"] = "lowercase";
|
|
16
|
-
})(TextTransform
|
|
16
|
+
})(TextTransform || (exports.TextTransform = TextTransform = {}));
|
|
17
17
|
var VerticalAlign;
|
|
18
18
|
(function (VerticalAlign) {
|
|
19
19
|
VerticalAlign["Super"] = "super";
|
|
20
20
|
VerticalAlign["Sub"] = "sub";
|
|
21
21
|
VerticalAlign["Unset"] = "unset";
|
|
22
|
-
})(VerticalAlign
|
|
22
|
+
})(VerticalAlign || (exports.VerticalAlign = VerticalAlign = {}));
|
|
23
23
|
var TextDecoration;
|
|
24
24
|
(function (TextDecoration) {
|
|
25
25
|
TextDecoration["Underline"] = "underline";
|
|
26
26
|
TextDecoration["None"] = "none";
|
|
27
|
-
})(TextDecoration
|
|
27
|
+
})(TextDecoration || (exports.TextDecoration = TextDecoration = {}));
|
|
@@ -5,4 +5,4 @@ var SectionHeightMode;
|
|
|
5
5
|
(function (SectionHeightMode) {
|
|
6
6
|
SectionHeightMode["ControlUnits"] = "control-units";
|
|
7
7
|
SectionHeightMode["ViewportHeightUnits"] = "viewport-height-units";
|
|
8
|
-
})(SectionHeightMode
|
|
8
|
+
})(SectionHeightMode || (exports.SectionHeightMode = SectionHeightMode = {}));
|
|
@@ -17,4 +17,4 @@ var KeyframeType;
|
|
|
17
17
|
KeyframeType["WordSpacing"] = "word-spacing";
|
|
18
18
|
KeyframeType["Blur"] = "blur";
|
|
19
19
|
KeyframeType["BackdropBlur"] = "backdrop-blur";
|
|
20
|
-
})(KeyframeType
|
|
20
|
+
})(KeyframeType || (exports.KeyframeType = KeyframeType = {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntrl-site/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Generic SDK for use in public websites.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -29,11 +29,14 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@types/ejs": "^3.1.2",
|
|
31
31
|
"@types/isomorphic-fetch": "^0.0.36",
|
|
32
|
+
"@types/ua-parser-js": "^0.7.39",
|
|
32
33
|
"commander": "^10.0.1",
|
|
33
34
|
"dotenv": "^16.1.3",
|
|
34
35
|
"ejs": "^3.1.9",
|
|
35
36
|
"isomorphic-fetch": "^3.0.0",
|
|
37
|
+
"mp4box": "^0.5.2",
|
|
36
38
|
"ts-node": "^10.9.1",
|
|
39
|
+
"ua-parser-js": "^1.0.37",
|
|
37
40
|
"url": "^0.11.0",
|
|
38
41
|
"zod": "^3.22.4"
|
|
39
42
|
},
|
|
@@ -44,6 +47,6 @@
|
|
|
44
47
|
"@types/node": "^18.11.7",
|
|
45
48
|
"jest": "^28.1.3",
|
|
46
49
|
"ts-jest": "^28.0.8",
|
|
47
|
-
"typescript": "^
|
|
50
|
+
"typescript": "^5.2.2"
|
|
48
51
|
}
|
|
49
52
|
}
|
|
@@ -23,7 +23,7 @@ describe('Client', () => {
|
|
|
23
23
|
it('returns page data', async () => {
|
|
24
24
|
const projectId = 'projectId';
|
|
25
25
|
const API_BASE_URL = 'api-test.cntrl.site';
|
|
26
|
-
const fetchesMap = {
|
|
26
|
+
const fetchesMap: Record<string, unknown> = {
|
|
27
27
|
[`https://${API_BASE_URL}/projects/${projectId}`]: projectMock,
|
|
28
28
|
[`https://${API_BASE_URL}/projects/${projectId}/articles/articleId`]: {
|
|
29
29
|
article: articleMock,
|
|
@@ -59,7 +59,7 @@ describe('Client', () => {
|
|
|
59
59
|
it('ignores page meta if it is not enabled and uses project meta instead', async () => {
|
|
60
60
|
const projectId = 'projectId';
|
|
61
61
|
const API_BASE_URL = 'api-test.cntrl.site';
|
|
62
|
-
const fetchesMap = {
|
|
62
|
+
const fetchesMap: Record<string, unknown> = {
|
|
63
63
|
[`https://${API_BASE_URL}/projects/${projectId}`]: projectMock,
|
|
64
64
|
[`https://${API_BASE_URL}/projects/${projectId}/articles/articleId2`]: {
|
|
65
65
|
article: articleMock,
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import UAParser from 'ua-parser-js';
|
|
2
|
+
import videoDecoder from '../VideoDecoder/VideoDecoder';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface ScrollVideoOptions {
|
|
6
|
+
src: string;
|
|
7
|
+
videoContainer: HTMLElement | string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ScrollPlaybackVideoManager {
|
|
11
|
+
private container?: HTMLElement;
|
|
12
|
+
private video?: HTMLVideoElement;
|
|
13
|
+
private isSafari?: boolean;
|
|
14
|
+
private currentTime = 0;
|
|
15
|
+
private targetTime = 0;
|
|
16
|
+
private canvas: HTMLCanvasElement | null = null;
|
|
17
|
+
private context: CanvasRenderingContext2D | null = null;
|
|
18
|
+
private frames: ImageBitmap[] = [];
|
|
19
|
+
private frameRate = 0;
|
|
20
|
+
private transitioning = false;
|
|
21
|
+
private debug: boolean = false;
|
|
22
|
+
private frameThreshold: number = 0;
|
|
23
|
+
private transitionSpeed: number = 10;
|
|
24
|
+
private useWebCodecs: boolean = true;
|
|
25
|
+
private resizeObserver: ResizeObserver;
|
|
26
|
+
|
|
27
|
+
constructor(options: ScrollVideoOptions) {
|
|
28
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
29
|
+
this.resize();
|
|
30
|
+
});
|
|
31
|
+
const {
|
|
32
|
+
src,
|
|
33
|
+
videoContainer
|
|
34
|
+
} = options;
|
|
35
|
+
if (typeof document !== 'object') {
|
|
36
|
+
console.error('ScrollVideo must be initiated in a DOM context');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!videoContainer) {
|
|
40
|
+
console.error('scrollVideoContainer must be a valid DOM object');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!src) {
|
|
44
|
+
console.error('Must provide valid video src to ScrollVideo');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.container = typeof videoContainer === 'string' ? document.getElementById(videoContainer)! : videoContainer;
|
|
49
|
+
this.resizeObserver.observe(this.container);
|
|
50
|
+
this.video = document.createElement('video');
|
|
51
|
+
this.video.src = src;
|
|
52
|
+
this.video.preload = 'auto';
|
|
53
|
+
this.video.tabIndex = 0;
|
|
54
|
+
this.video.playsInline = true;
|
|
55
|
+
this.video.muted = true;
|
|
56
|
+
this.video.pause();
|
|
57
|
+
this.video.load();
|
|
58
|
+
this.container.appendChild(this.video);
|
|
59
|
+
const browserEngine = new UAParser().getEngine();
|
|
60
|
+
this.isSafari = browserEngine.name === 'WebKit';
|
|
61
|
+
if (this.debug && this.isSafari) console.info('Safari browser detected');
|
|
62
|
+
this.video.addEventListener('loadedmetadata', () => this.setTargetTimePercent(0, true), { once: true });
|
|
63
|
+
this.video.addEventListener('progress', this.resize);
|
|
64
|
+
this.decodeVideo();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private setCoverStyle(el: any) {
|
|
68
|
+
if (el && this.container) {
|
|
69
|
+
el.style.position = 'absolute';
|
|
70
|
+
el.style.top = '50%';
|
|
71
|
+
el.style.left = '50%';
|
|
72
|
+
el.style.transform = 'translate(-50%, -50%)';
|
|
73
|
+
const { width: containerWidth, height: containerHeight } = this.container.getBoundingClientRect();
|
|
74
|
+
const width = el.videoWidth || el.width;
|
|
75
|
+
const height = el.videoHeight || el.height;
|
|
76
|
+
if (containerWidth / containerHeight > width / height) {
|
|
77
|
+
el.style.width = '100%';
|
|
78
|
+
el.style.height = 'auto';
|
|
79
|
+
} else {
|
|
80
|
+
el.style.height = '100%';
|
|
81
|
+
el.style.width = 'auto';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private resize = () => {
|
|
87
|
+
if (this.debug) console.info('ScrollVideo resizing...');
|
|
88
|
+
if (this.canvas) {
|
|
89
|
+
this.setCoverStyle(this.canvas);
|
|
90
|
+
} else if (this.video) {
|
|
91
|
+
this.setCoverStyle(this.video);
|
|
92
|
+
}
|
|
93
|
+
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
private decodeVideo = () => {
|
|
97
|
+
if (!this.video) return;
|
|
98
|
+
if (this.useWebCodecs && this.video.src) {
|
|
99
|
+
videoDecoder(this.video.src, (frame: ImageBitmap) => {
|
|
100
|
+
this.frames.push(frame);
|
|
101
|
+
},
|
|
102
|
+
this.debug,
|
|
103
|
+
).then(() => {
|
|
104
|
+
if (!this.video || !this.container) return
|
|
105
|
+
if (this.frames.length === 0) {
|
|
106
|
+
if (this.debug) console.error('No frames were received from webCodecs');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.frameRate = this.frames.length / this.video.duration;
|
|
110
|
+
if (this.debug) console.info('Received', this.frames.length, 'frames');
|
|
111
|
+
this.canvas = document.createElement('canvas');
|
|
112
|
+
this.context = this.canvas.getContext('2d')!;
|
|
113
|
+
this.video.style.display = 'none';
|
|
114
|
+
this.container.appendChild(this.canvas);
|
|
115
|
+
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
116
|
+
}).catch(() => {
|
|
117
|
+
if (this.debug) console.error('Error encountered while decoding video');
|
|
118
|
+
this.frames = [];
|
|
119
|
+
this.video!.load();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private paintCanvasFrame(frameNum: number) {
|
|
125
|
+
if (this.canvas) {
|
|
126
|
+
const frameIdx = Math.min(frameNum, this.frames.length - 1);
|
|
127
|
+
const currFrame = this.frames[frameIdx];
|
|
128
|
+
if (currFrame && this.container) {
|
|
129
|
+
if (this.debug) console.info('Painting frame', frameIdx);
|
|
130
|
+
this.canvas.width = currFrame.width;
|
|
131
|
+
this.canvas.height = currFrame.height;
|
|
132
|
+
const { width, height } = this.container.getBoundingClientRect();
|
|
133
|
+
this.resetCanvasDimensions(width, height, currFrame.width, currFrame.height);
|
|
134
|
+
this.context!.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private transitionToTargetTime(jump: boolean) {
|
|
140
|
+
if (!this.video) return;
|
|
141
|
+
if (this.debug) console.info('Transitioning targetTime:', this.targetTime, 'currentTime:', this.currentTime);
|
|
142
|
+
if (isNaN(this.targetTime) || Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) {
|
|
143
|
+
this.video.pause();
|
|
144
|
+
this.transitioning = false;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Make sure we don't go out of time bounds
|
|
148
|
+
if (this.targetTime > this.video.duration) {
|
|
149
|
+
this.targetTime = this.video.duration;
|
|
150
|
+
}
|
|
151
|
+
if (this.targetTime < 0) {
|
|
152
|
+
this.targetTime = 0;
|
|
153
|
+
}
|
|
154
|
+
// How far forward we need to transition
|
|
155
|
+
const transitionForward = this.targetTime - this.currentTime;
|
|
156
|
+
if (this.canvas) {
|
|
157
|
+
// Update currentTime and paint the closest frame
|
|
158
|
+
this.currentTime += transitionForward / (256 / this.transitionSpeed);
|
|
159
|
+
// If jump, we go directly to the frame
|
|
160
|
+
if (jump) { this.currentTime = this.targetTime }
|
|
161
|
+
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
|
|
162
|
+
} else if (jump || this.isSafari || this.targetTime - this.currentTime < 0) {
|
|
163
|
+
this.video.pause();
|
|
164
|
+
this.currentTime += transitionForward / (64 / this.transitionSpeed);
|
|
165
|
+
// If jump, we go directly to the frame
|
|
166
|
+
if (jump) { this.currentTime = this.targetTime;}
|
|
167
|
+
this.video.currentTime = this.currentTime;
|
|
168
|
+
} else {
|
|
169
|
+
// Otherwise, we play the video and adjust the playbackRate to get a smoother
|
|
170
|
+
// animation effect.
|
|
171
|
+
const playbackRate = Math.max(Math.min(transitionForward * 4, this.transitionSpeed, 16), 1);
|
|
172
|
+
if (this.debug) console.info('ScrollVideo playbackRate:', playbackRate);
|
|
173
|
+
if (!isNaN(playbackRate)) {
|
|
174
|
+
this.video.playbackRate = playbackRate;
|
|
175
|
+
this.video.play();
|
|
176
|
+
}
|
|
177
|
+
this.currentTime = this.video.currentTime;
|
|
178
|
+
}
|
|
179
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
180
|
+
requestAnimationFrame(() => this.transitionToTargetTime(jump));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private resetCanvasDimensions(w: number, h: number, frameW: number, frameH: number) {
|
|
185
|
+
if (!this.canvas) return;
|
|
186
|
+
if (w / h > frameW / frameH) {
|
|
187
|
+
this.canvas.style.width = '100%';
|
|
188
|
+
this.canvas.style.height = 'auto';
|
|
189
|
+
} else {
|
|
190
|
+
this.canvas.style.height = '100%';
|
|
191
|
+
this.canvas.style.width = 'auto';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
setTargetTimePercent(setPercentage: number, jump: boolean = true): void {
|
|
196
|
+
if (!this.video) return;
|
|
197
|
+
this.targetTime = Math.max(Math.min(setPercentage, 1), 0)
|
|
198
|
+
* (this.frames.length && this.frameRate ? this.frames.length / this.frameRate : this.video.duration);
|
|
199
|
+
if (!jump && Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) return;
|
|
200
|
+
if (!jump && this.transitioning) return;
|
|
201
|
+
if (!this.canvas && !this.video.paused) this.video.play();
|
|
202
|
+
this.transitioning = true;
|
|
203
|
+
this.transitionToTargetTime(jump);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
destroy(): void {
|
|
207
|
+
this.resizeObserver.unobserve(this.container!);
|
|
208
|
+
this.video?.removeEventListener('progress', this.resize);
|
|
209
|
+
if (this.debug) console.info('Destroying ScrollVideo');
|
|
210
|
+
if (this.container) this.container.innerHTML = '';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { Client as CntrlClient } from './Client/Client';
|
|
3
3
|
export { FontFaceGenerator } from './FontFaceGenerator/FontFaceGenerator';
|
|
4
4
|
export { getLayoutStyles, getLayoutMediaQuery } from './utils';
|
|
5
|
+
export {ScrollPlaybackVideoManager} from './ScrollPlaybackVideoManager/ScrollPlaybackVideoManager';
|
|
5
6
|
|
|
6
7
|
// enums
|
|
7
8
|
export { SectionHeightMode } from './types/article/Section';
|