image-to-pixel 3.0.1 → 3.1.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.
- package/README.md +69 -69
- package/dist/editor.css +1 -1
- package/dist/editor.css.map +2 -2
- package/dist/editor.esm.js +60 -60
- package/dist/editor.esm.js.map +4 -4
- package/dist/image-to-pixel.bundle.esm.js +10 -10
- package/dist/image-to-pixel.bundle.esm.js.map +3 -3
- package/dist/image-to-pixel.cjs +2 -2
- package/dist/image-to-pixel.cjs.map +2 -2
- package/dist/image-to-pixel.esm.js +2 -2
- package/dist/image-to-pixel.esm.js.map +2 -2
- package/dist/image-to-pixel.js +2 -2
- package/dist/image-to-pixel.js.map +2 -2
- package/dist/image-to-pixel.min.js +9 -9
- package/dist/image-to-pixel.min.js.map +3 -3
- package/dist/video.esm.js +2 -2
- package/dist/video.esm.js.map +4 -4
- package/lib/core/video/audio.js +159 -0
- package/lib/core/video/frames.js +182 -144
- package/lib/core/video/mux-mp4.js +320 -131
- package/lib/core/video/mux-webm.js +168 -127
- package/lib/editor/exporter.js +225 -185
- package/lib/editor/icons.js +65 -61
- package/lib/editor/index.js +1002 -793
- package/lib/editor/inspector.js +343 -328
- package/lib/editor/library.js +188 -182
- package/lib/editor/styles/editor.css +418 -385
- package/lib/editor/workspace.js +62 -0
- package/lib/index.js +31 -31
- package/lib/video.js +122 -92
- package/package.json +92 -92
- package/types/video.d.ts +99 -43
|
@@ -1,127 +1,168 @@
|
|
|
1
|
-
// Minimal WebM (Matroska) muxer for one VP8/VP9/AV1 video track
|
|
2
|
-
// a SeekHead and Cues so players can seek.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
1
|
+
// Minimal WebM (Matroska) muxer for one VP8/VP9/AV1 video track and, optionally, one Opus audio track.
|
|
2
|
+
// Buffered in memory, written with a SeekHead and Cues so players can seek.
|
|
3
|
+
// Input: encoded frames from WebCodecs' VideoEncoder (and AudioEncoder for the audio).
|
|
4
|
+
|
|
5
|
+
import { opusHead, parseOpusHead } from './audio.js';
|
|
6
|
+
|
|
7
|
+
const ENC = new TextEncoder();
|
|
8
|
+
|
|
9
|
+
/** EBML variable-length size, always 8 bytes (simple and valid for any size). */
|
|
10
|
+
function size8(n) {
|
|
11
|
+
const b = new Uint8Array(8);
|
|
12
|
+
b[0] = 0x01;
|
|
13
|
+
let v = n;
|
|
14
|
+
for (let i = 7; i >= 1; i--) { b[i] = v % 256; v = Math.floor(v / 256); }
|
|
15
|
+
return b;
|
|
16
|
+
}
|
|
17
|
+
function idBytes(id) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (let v = id; v > 0; v = Math.floor(v / 256)) out.unshift(v % 256);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function concat(parts) {
|
|
23
|
+
let n = 0; for (const p of parts) n += p.length;
|
|
24
|
+
const out = new Uint8Array(n); let o = 0;
|
|
25
|
+
for (const p of parts) { out.set(p, o); o += p.length; }
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/** An element: id, 8-byte size, data. */
|
|
29
|
+
const el = (id, data) => concat([Uint8Array.from(idBytes(id)), size8(data.length), data]);
|
|
30
|
+
const uint = (id, v, bytes = 0) => {
|
|
31
|
+
const b = [];
|
|
32
|
+
for (let x = v; x > 0 || b.length === 0; x = Math.floor(x / 256)) b.unshift(x % 256);
|
|
33
|
+
while (b.length < bytes) b.unshift(0);
|
|
34
|
+
return el(id, Uint8Array.from(b));
|
|
35
|
+
};
|
|
36
|
+
const str = (id, s) => el(id, ENC.encode(s));
|
|
37
|
+
const float64 = (id, v) => { const b = new Uint8Array(8); new DataView(b.buffer).setFloat64(0, v); return el(id, b); };
|
|
38
|
+
const master = (id, children) => el(id, concat(children));
|
|
39
|
+
|
|
40
|
+
const ID = {
|
|
41
|
+
EBML: 0x1A45DFA3, EBMLVersion: 0x4286, EBMLReadVersion: 0x42F7, EBMLMaxIDLength: 0x42F2, EBMLMaxSizeLength: 0x42F3,
|
|
42
|
+
DocType: 0x4282, DocTypeVersion: 0x4287, DocTypeReadVersion: 0x4285,
|
|
43
|
+
Segment: 0x18538067, SeekHead: 0x114D9B74, Seek: 0x4DBB, SeekID: 0x53AB, SeekPosition: 0x53AC,
|
|
44
|
+
Info: 0x1549A966, TimestampScale: 0x2AD7B1, Duration: 0x4489, MuxingApp: 0x4D80, WritingApp: 0x5741,
|
|
45
|
+
Tracks: 0x1654AE6B, TrackEntry: 0xAE, TrackNumber: 0xD7, TrackUID: 0x73C5, TrackType: 0x83, FlagLacing: 0x9C,
|
|
46
|
+
CodecID: 0x86, CodecPrivate: 0x63A2, DefaultDuration: 0x23E383, Video: 0xE0, PixelWidth: 0xB0, PixelHeight: 0xBA,
|
|
47
|
+
CodecDelay: 0x56AA, SeekPreRoll: 0x56BB, Audio: 0xE1, SamplingFrequency: 0xB5, Channels: 0x9F,
|
|
48
|
+
Cluster: 0x1F43B675, Timestamp: 0xE7, SimpleBlock: 0xA3,
|
|
49
|
+
Cues: 0x1C53BB6B, CuePoint: 0xBB, CueTime: 0xB3, CueTrackPositions: 0xB7, CueTrack: 0xF7, CueClusterPosition: 0xF1,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const CODEC_IDS = { vp8: 'V_VP8', vp9: 'V_VP9', av1: 'V_AV1' };
|
|
53
|
+
const VIDEO = 1, AUDIO = 2; // track numbers
|
|
54
|
+
|
|
55
|
+
const toBytes = d => new Uint8Array(d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength));
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {{ width: number, height: number, codec: 'vp8' | 'vp9' | 'av1', fps?: number,
|
|
59
|
+
* audio?: { codec: 'opus', sampleRate: number, numberOfChannels: number } }} opts
|
|
60
|
+
* audio: add an Opus track (WebM holds Opus or Vorbis only). Feed it with addAudioChunk() or addAudioFrame().
|
|
61
|
+
*/
|
|
62
|
+
export function createWebmMuxer({ width, height, codec = 'vp9', fps = 0, audio = null }) {
|
|
63
|
+
if (audio && audio.codec && audio.codec !== 'opus') throw new Error(`WebM audio must be Opus, not ${audio.codec}`);
|
|
64
|
+
const frames = []; // video: { data, key, ms }
|
|
65
|
+
const audioFrames = []; // audio: { data, ms, dms }
|
|
66
|
+
let codecPrivate = null;
|
|
67
|
+
let audioPrivate = null;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
addChunk(chunk, meta) {
|
|
71
|
+
if (meta && meta.decoderConfig && meta.decoderConfig.description && !codecPrivate) codecPrivate = toBytes(meta.decoderConfig.description);
|
|
72
|
+
const data = new Uint8Array(chunk.byteLength);
|
|
73
|
+
chunk.copyTo(data);
|
|
74
|
+
frames.push({ data, key: chunk.type === 'key', ms: chunk.timestamp / 1000 });
|
|
75
|
+
},
|
|
76
|
+
/** Lower-level: add one encoded frame. timestamp in microseconds. */
|
|
77
|
+
addFrame(data, { key, timestamp }) { frames.push({ data, key: !!key, ms: timestamp / 1000 }); },
|
|
78
|
+
|
|
79
|
+
/** Use with an AudioEncoder `output` callback (needs the `audio` option). */
|
|
80
|
+
addAudioChunk(chunk, meta) {
|
|
81
|
+
if (meta && meta.decoderConfig && meta.decoderConfig.description && !audioPrivate) audioPrivate = toBytes(meta.decoderConfig.description);
|
|
82
|
+
const data = new Uint8Array(chunk.byteLength);
|
|
83
|
+
chunk.copyTo(data);
|
|
84
|
+
audioFrames.push({ data, ms: chunk.timestamp / 1000, dms: (chunk.duration || 0) / 1000 });
|
|
85
|
+
},
|
|
86
|
+
/** Lower-level: add one Opus packet, in order. timestamp/duration in microseconds. */
|
|
87
|
+
addAudioFrame(data, { timestamp, duration }) { audioFrames.push({ data, ms: timestamp / 1000, dms: (duration || 0) / 1000 }); },
|
|
88
|
+
|
|
89
|
+
/** @returns {Uint8Array} the .webm file */
|
|
90
|
+
finish() {
|
|
91
|
+
if (!frames.length) throw new Error('No video frames were encoded');
|
|
92
|
+
const hasAudio = !!(audio && audioFrames.length);
|
|
93
|
+
// VP8/VP9/AV1 frames from WebCodecs arrive in decode order, which is the order they're stored in
|
|
94
|
+
const frameMs = fps ? 1000 / fps : (frames.length > 1 ? (frames[frames.length - 1].ms - frames[0].ms) / (frames.length - 1) : 33.333);
|
|
95
|
+
let duration = frames[frames.length - 1].ms + frameMs;
|
|
96
|
+
if (hasAudio) { const a = audioFrames[audioFrames.length - 1]; duration = Math.max(duration, a.ms + (a.dms || 20)); }
|
|
97
|
+
|
|
98
|
+
const info = master(ID.Info, [
|
|
99
|
+
uint(ID.TimestampScale, 1000000), // 1 ms
|
|
100
|
+
float64(ID.Duration, duration),
|
|
101
|
+
str(ID.MuxingApp, 'image-to-pixel'), str(ID.WritingApp, 'image-to-pixel'),
|
|
102
|
+
]);
|
|
103
|
+
const trackEntries = [master(ID.TrackEntry, [
|
|
104
|
+
uint(ID.TrackNumber, VIDEO), uint(ID.TrackUID, VIDEO), uint(ID.TrackType, 1), uint(ID.FlagLacing, 0),
|
|
105
|
+
str(ID.CodecID, CODEC_IDS[codec] || codec),
|
|
106
|
+
...(codecPrivate ? [el(ID.CodecPrivate, codecPrivate)] : []),
|
|
107
|
+
uint(ID.DefaultDuration, Math.round(frameMs * 1e6)),
|
|
108
|
+
master(ID.Video, [uint(ID.PixelWidth, width), uint(ID.PixelHeight, height)]),
|
|
109
|
+
])];
|
|
110
|
+
if (hasAudio) {
|
|
111
|
+
// Matroska requires the OpusHead as CodecPrivate; build one when the encoder didn't provide it
|
|
112
|
+
const head = parseOpusHead(audioPrivate) ? audioPrivate : opusHead(audio.numberOfChannels, audio.sampleRate);
|
|
113
|
+
const preSkip = parseOpusHead(head).preSkip;
|
|
114
|
+
trackEntries.push(master(ID.TrackEntry, [
|
|
115
|
+
uint(ID.TrackNumber, AUDIO), uint(ID.TrackUID, AUDIO), uint(ID.TrackType, 2), uint(ID.FlagLacing, 0),
|
|
116
|
+
str(ID.CodecID, 'A_OPUS'),
|
|
117
|
+
el(ID.CodecPrivate, head),
|
|
118
|
+
uint(ID.CodecDelay, Math.round(preSkip * 1e9 / 48000)), // nanoseconds
|
|
119
|
+
uint(ID.SeekPreRoll, 80000000), // 80 ms, as the Opus mapping recommends
|
|
120
|
+
master(ID.Audio, [float64(ID.SamplingFrequency, audio.sampleRate), uint(ID.Channels, audio.numberOfChannels)]),
|
|
121
|
+
]));
|
|
122
|
+
}
|
|
123
|
+
const tracks = master(ID.Tracks, trackEntries);
|
|
124
|
+
|
|
125
|
+
// Blocks of both tracks in time order (video first on a tie, so a cluster starts on its keyframe).
|
|
126
|
+
const blocks = frames.map((f, i) => ({ track: VIDEO, data: f.data, key: f.key, ms: f.ms, order: i }));
|
|
127
|
+
if (hasAudio) audioFrames.forEach((f, i) => blocks.push({ track: AUDIO, data: f.data, key: true, ms: f.ms, order: frames.length + i }));
|
|
128
|
+
if (hasAudio) blocks.sort((a, b) => a.ms - b.ms || a.track - b.track || a.order - b.order);
|
|
129
|
+
|
|
130
|
+
// clusters: a new one at each video keyframe, and before the 16-bit relative timestamp overflows
|
|
131
|
+
const clusters = [];
|
|
132
|
+
let cur = null;
|
|
133
|
+
for (const b of blocks) {
|
|
134
|
+
const t = Math.round(b.ms);
|
|
135
|
+
const videoKey = b.track === VIDEO && b.key;
|
|
136
|
+
if (!cur || videoKey || t - cur.start > 30000) { cur = { start: t, key: videoKey, blocks: [] }; clusters.push(cur); }
|
|
137
|
+
const head = new Uint8Array(4);
|
|
138
|
+
head[0] = 0x80 | b.track; // track number as a 1-byte EBML vint
|
|
139
|
+
new DataView(head.buffer).setInt16(1, t - cur.start);
|
|
140
|
+
head[3] = b.key ? 0x80 : 0x00;
|
|
141
|
+
cur.blocks.push(el(ID.SimpleBlock, concat([head, b.data])));
|
|
142
|
+
}
|
|
143
|
+
const clusterBytes = clusters.map(c => master(ID.Cluster, [uint(ID.Timestamp, c.start), ...c.blocks]));
|
|
144
|
+
|
|
145
|
+
// SeekHead with fixed-width positions, so its size is known before the positions are
|
|
146
|
+
const seekHead = (posInfo, posTracks, posCues) => master(ID.SeekHead, [
|
|
147
|
+
[ID.Info, posInfo], [ID.Tracks, posTracks], [ID.Cues, posCues],
|
|
148
|
+
].map(([id, pos]) => master(ID.Seek, [el(ID.SeekID, Uint8Array.from(idBytes(id))), uint(ID.SeekPosition, pos, 8)])));
|
|
149
|
+
const shSize = seekHead(0, 0, 0).length;
|
|
150
|
+
const posInfo = shSize, posTracks = posInfo + info.length;
|
|
151
|
+
let pos = posTracks + tracks.length;
|
|
152
|
+
const cuePoints = [];
|
|
153
|
+
clusters.forEach((c, i) => {
|
|
154
|
+
if (c.key) cuePoints.push(master(ID.CuePoint, [uint(ID.CueTime, c.start), master(ID.CueTrackPositions, [uint(ID.CueTrack, VIDEO), uint(ID.CueClusterPosition, pos)])]));
|
|
155
|
+
pos += clusterBytes[i].length;
|
|
156
|
+
});
|
|
157
|
+
const cues = master(ID.Cues, cuePoints.length ? cuePoints : [master(ID.CuePoint, [uint(ID.CueTime, 0), master(ID.CueTrackPositions, [uint(ID.CueTrack, VIDEO), uint(ID.CueClusterPosition, posTracks + tracks.length)])])]);
|
|
158
|
+
const posCues = pos;
|
|
159
|
+
|
|
160
|
+
const header = master(ID.EBML, [
|
|
161
|
+
uint(ID.EBMLVersion, 1), uint(ID.EBMLReadVersion, 1), uint(ID.EBMLMaxIDLength, 4), uint(ID.EBMLMaxSizeLength, 8),
|
|
162
|
+
str(ID.DocType, 'webm'), uint(ID.DocTypeVersion, 4), uint(ID.DocTypeReadVersion, 2),
|
|
163
|
+
]);
|
|
164
|
+
const segment = master(ID.Segment, [seekHead(posInfo, posTracks, posCues), info, tracks, ...clusterBytes, cues]);
|
|
165
|
+
return concat([header, segment]);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|