bare-media 1.8.0 → 2.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.
@@ -1,212 +0,0 @@
1
- import Hyperschema from 'hyperschema'
2
-
3
- import { SCHEMA_DIR } from './constants'
4
-
5
- export const schema = Hyperschema.from(SCHEMA_DIR)
6
- const media = schema.namespace('media')
7
-
8
- media.register({
9
- name: 'dimensions',
10
- fields: [
11
- {
12
- name: 'width',
13
- type: 'uint',
14
- required: true
15
- },
16
- {
17
- name: 'height',
18
- type: 'uint',
19
- required: true
20
- }
21
- ]
22
- })
23
-
24
- media.register({
25
- name: 'metadata',
26
- fields: [
27
- {
28
- name: 'mimetype',
29
- type: 'string'
30
- },
31
- {
32
- name: 'dimensions',
33
- type: '@media/dimensions'
34
- },
35
- {
36
- name: 'duration',
37
- type: 'uint'
38
- }
39
- ]
40
- })
41
-
42
- media.register({
43
- name: 'file',
44
- fields: [
45
- {
46
- name: 'metadata',
47
- type: '@media/metadata'
48
- },
49
- {
50
- name: 'inlined',
51
- type: 'string'
52
- },
53
- {
54
- name: 'buffer',
55
- type: 'buffer'
56
- }
57
- ]
58
- })
59
-
60
- media.register({
61
- name: 'create-preview-request',
62
- fields: [
63
- {
64
- name: 'path',
65
- type: 'string'
66
- },
67
- {
68
- name: 'httpLink',
69
- type: 'string'
70
- },
71
- {
72
- name: 'buffer',
73
- type: 'buffer'
74
- },
75
- {
76
- name: 'mimetype',
77
- type: 'string'
78
- },
79
- {
80
- name: 'maxWidth',
81
- type: 'uint'
82
- },
83
- {
84
- name: 'maxHeight',
85
- type: 'uint'
86
- },
87
- {
88
- name: 'maxFrames',
89
- type: 'uint'
90
- },
91
- {
92
- name: 'maxBytes',
93
- type: 'uint'
94
- },
95
- {
96
- name: 'format',
97
- type: 'string'
98
- },
99
- {
100
- name: 'encoding',
101
- type: 'string'
102
- }
103
- ]
104
- })
105
-
106
- media.register({
107
- name: 'create-preview-response',
108
- fields: [
109
- {
110
- name: 'metadata',
111
- type: '@media/metadata',
112
- required: true
113
- },
114
- {
115
- name: 'preview',
116
- type: '@media/file',
117
- required: true
118
- }
119
- ]
120
- })
121
-
122
- media.register({
123
- name: 'decode-image-request',
124
- fields: [
125
- {
126
- name: 'path',
127
- type: 'string'
128
- },
129
- {
130
- name: 'httpLink',
131
- type: 'string'
132
- },
133
- {
134
- name: 'buffer',
135
- type: 'buffer'
136
- },
137
- {
138
- name: 'mimetype',
139
- type: 'string'
140
- }
141
- ]
142
- })
143
-
144
- media.register({
145
- name: 'decode-image-response',
146
- fields: [
147
- {
148
- name: 'metadata',
149
- type: '@media/metadata'
150
- },
151
- {
152
- name: 'data',
153
- type: 'buffer'
154
- }
155
- ]
156
- })
157
-
158
- media.register({
159
- name: 'crop-image-request',
160
- fields: [
161
- {
162
- name: 'path',
163
- type: 'string'
164
- },
165
- {
166
- name: 'httpLink',
167
- type: 'string'
168
- },
169
- {
170
- name: 'buffer',
171
- type: 'buffer'
172
- },
173
- {
174
- name: 'mimetype',
175
- type: 'string'
176
- },
177
- {
178
- name: 'left',
179
- type: 'uint'
180
- },
181
- {
182
- name: 'top',
183
- type: 'uint'
184
- },
185
- {
186
- name: 'width',
187
- type: 'uint'
188
- },
189
- {
190
- name: 'height',
191
- type: 'uint'
192
- },
193
- {
194
- name: 'format',
195
- type: 'string'
196
- }
197
- ]
198
- })
199
-
200
- media.register({
201
- name: 'crop-image-response',
202
- fields: [
203
- {
204
- name: 'metadata',
205
- type: '@media/metadata'
206
- },
207
- {
208
- name: 'data',
209
- type: 'buffer'
210
- }
211
- ]
212
- })
package/worker/index.js DELETED
@@ -1,25 +0,0 @@
1
- import worker from 'cross-worker'
2
- import uncaughts from 'uncaughts'
3
-
4
- import HRPC from '../shared/spec/hrpc'
5
-
6
- import * as media from './media'
7
- import { log } from './util'
8
-
9
- log('Worker started 🚀')
10
-
11
- const stream = worker.stream()
12
-
13
- stream.on('end', () => stream.end())
14
- stream.on('error', (err) => console.error(err))
15
- stream.on('close', () => Bare.exit(0))
16
-
17
- const rpc = new HRPC(stream)
18
-
19
- rpc.onCreatePreview(media.createPreview)
20
- rpc.onDecodeImage(media.decodeImage)
21
- rpc.onCropImage(media.cropImage)
22
-
23
- uncaughts.on((err) => {
24
- log('Uncaught error:', err)
25
- })
package/worker/media.js DELETED
@@ -1,274 +0,0 @@
1
- import b4a from 'b4a'
2
-
3
- import {
4
- importCodec,
5
- isCodecSupported,
6
- supportsQuality
7
- } from '../shared/codecs.js'
8
- import { getBuffer, detectMimeType, calculateFitDimensions } from './util'
9
-
10
- const DEFAULT_PREVIEW_FORMAT = 'image/webp'
11
-
12
- const animatableMimetypes = ['image/webp']
13
-
14
- export async function createPreview({
15
- path,
16
- httpLink,
17
- buffer,
18
- mimetype,
19
- maxWidth,
20
- maxHeight,
21
- maxFrames,
22
- maxBytes,
23
- format,
24
- encoding
25
- }) {
26
- format = format || DEFAULT_PREVIEW_FORMAT
27
-
28
- const buff = await getBuffer({ path, httpLink, buffer })
29
- mimetype = mimetype || detectMimeType(buff, path)
30
-
31
- if (!isCodecSupported(mimetype)) {
32
- throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
33
- }
34
-
35
- const rgba = await decodeImageToRGBA(buff, mimetype, maxFrames)
36
- const { width, height } = rgba
37
-
38
- const maybeResizedRGBA = await resizeRGBA(rgba, maxWidth, maxHeight)
39
-
40
- let preview = await encodeImageFromRGBA(maybeResizedRGBA, format)
41
-
42
- // quality reduction
43
-
44
- if (maxBytes && preview.byteLength > maxBytes && supportsQuality(format)) {
45
- const MIN_QUALITY = 50
46
- for (let quality = 80; quality >= MIN_QUALITY; quality -= 15) {
47
- preview = await encodeImageFromRGBA(maybeResizedRGBA, format, { quality })
48
- if (preview.byteLength <= maxBytes) {
49
- break
50
- }
51
- }
52
- }
53
-
54
- // fps reduction
55
-
56
- if (
57
- maxBytes &&
58
- preview.byteLength > maxBytes &&
59
- maybeResizedRGBA.frames?.length > 1
60
- ) {
61
- const quality = 75
62
-
63
- // drop every n frame
64
-
65
- for (const dropEvery of [4, 3, 2]) {
66
- const frames = maybeResizedRGBA.frames.filter(
67
- (frame, index) => index % dropEvery !== 0
68
- )
69
- const filtered = { ...maybeResizedRGBA, frames }
70
- preview = await encodeImageFromRGBA(filtered, format, { quality })
71
- if (!maxBytes || preview.byteLength <= maxBytes) {
72
- break
73
- }
74
- }
75
-
76
- // cap to 25 frames
77
-
78
- if (preview.byteLength > maxBytes) {
79
- const frames = maybeResizedRGBA.frames
80
- .slice(0, 50)
81
- .filter((frame, index) => index % 2 === 0)
82
- const capped = { ...maybeResizedRGBA, frames }
83
- preview = await encodeImageFromRGBA(capped, format, { quality })
84
- }
85
-
86
- // take only one frame
87
-
88
- if (preview.byteLength > maxBytes) {
89
- const oneFrame = {
90
- ...maybeResizedRGBA,
91
- frames: maybeResizedRGBA.frames.slice(0, 1)
92
- }
93
- preview = await encodeImageFromRGBA(oneFrame, format)
94
- }
95
- }
96
-
97
- if (maxBytes && preview.byteLength > maxBytes) {
98
- throw new Error(
99
- `Could not create preview under maxBytes, reached ${preview.byteLength} bytes`
100
- )
101
- }
102
-
103
- const encoded =
104
- encoding === 'base64'
105
- ? { inlined: b4a.toString(preview, 'base64') }
106
- : { buffer: preview }
107
-
108
- return {
109
- metadata: {
110
- dimensions: { width, height }
111
- },
112
- preview: {
113
- metadata: {
114
- mimetype: format,
115
- dimensions: {
116
- width: maybeResizedRGBA.width,
117
- height: maybeResizedRGBA.height
118
- }
119
- },
120
- ...encoded
121
- }
122
- }
123
- }
124
-
125
- export async function decodeImage({ path, httpLink, buffer, mimetype }) {
126
- const buff = await getBuffer({ path, httpLink, buffer })
127
- mimetype = mimetype || detectMimeType(buff, path)
128
-
129
- if (!isCodecSupported(mimetype)) {
130
- throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
131
- }
132
-
133
- const rgba = await decodeImageToRGBA(buff, mimetype)
134
- const { width, height, data } = rgba
135
-
136
- return {
137
- metadata: {
138
- dimensions: { width, height }
139
- },
140
- data
141
- }
142
- }
143
-
144
- export async function cropImage({
145
- path,
146
- httpLink,
147
- buffer,
148
- mimetype,
149
- left,
150
- top,
151
- width,
152
- height,
153
- format
154
- }) {
155
- const buff = await getBuffer({ path, httpLink, buffer })
156
- mimetype = mimetype || detectMimeType(buff, path)
157
-
158
- if (!isCodecSupported(mimetype)) {
159
- throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
160
- }
161
-
162
- const rgba = await decodeImageToRGBA(buff, mimetype)
163
-
164
- const cropped = await cropRGBA(rgba, left, top, width, height)
165
-
166
- const data = await encodeImageFromRGBA(cropped, format || mimetype)
167
-
168
- return {
169
- metadata: {
170
- dimensions: {
171
- width: rgba.width,
172
- height: rgba.height
173
- }
174
- },
175
- data
176
- }
177
- }
178
-
179
- async function decodeImageToRGBA(buffer, mimetype, maxFrames) {
180
- let rgba
181
-
182
- const codec = await importCodec(mimetype)
183
-
184
- if (animatableMimetypes.includes(mimetype)) {
185
- const { width, height, loops, frames } = codec.decodeAnimated(buffer)
186
- const data = []
187
- for (const frame of frames) {
188
- if (maxFrames > 0 && data.length >= maxFrames) break
189
- data.push(frame)
190
- }
191
- rgba = { width, height, loops, frames: data }
192
- } else {
193
- rgba = codec.decode(buffer)
194
- }
195
-
196
- return rgba
197
- }
198
-
199
- async function encodeImageFromRGBA(rgba, format, opts) {
200
- const codec = await importCodec(format)
201
-
202
- let encoded
203
- if (Array.isArray(rgba.frames)) {
204
- encoded = codec.encodeAnimated(rgba, opts)
205
- } else {
206
- encoded = codec.encode(rgba, opts)
207
- }
208
-
209
- return encoded
210
- }
211
-
212
- async function resizeRGBA(rgba, maxWidth, maxHeight) {
213
- const { width, height } = rgba
214
-
215
- let maybeResizedRGBA
216
-
217
- if (maxWidth && maxHeight && (width > maxWidth || height > maxHeight)) {
218
- const { resize } = await import('bare-image-resample')
219
- const dimensions = calculateFitDimensions(
220
- width,
221
- height,
222
- maxWidth,
223
- maxHeight
224
- )
225
- if (Array.isArray(rgba.frames)) {
226
- const frames = []
227
- for (const frame of rgba.frames) {
228
- const resized = resize(frame, dimensions.width, dimensions.height)
229
- frames.push({ ...resized, timestamp: frame.timestamp })
230
- }
231
- maybeResizedRGBA = {
232
- width: frames[0].width,
233
- height: frames[0].height,
234
- loops: rgba.loops,
235
- frames
236
- }
237
- } else {
238
- maybeResizedRGBA = resize(rgba, dimensions.width, dimensions.height)
239
- }
240
- } else {
241
- maybeResizedRGBA = rgba
242
- }
243
-
244
- return maybeResizedRGBA
245
- }
246
-
247
- async function cropRGBA(rgba, left, top, width, height) {
248
- if (
249
- left < 0 ||
250
- top < 0 ||
251
- width <= 0 ||
252
- height <= 0 ||
253
- left + width > rgba.width ||
254
- top + height > rgba.height
255
- ) {
256
- throw new Error('Crop rectangle out of bounds')
257
- }
258
-
259
- const data = Buffer.alloc(width * height * 4)
260
-
261
- for (let y = 0; y < height; y++) {
262
- for (let x = 0; x < width; x++) {
263
- const srcIndex = ((y + top) * rgba.width + (x + left)) * 4
264
- const dstIndex = (y * width + x) * 4
265
-
266
- data[dstIndex] = rgba.data[srcIndex]
267
- data[dstIndex + 1] = rgba.data[srcIndex + 1]
268
- data[dstIndex + 2] = rgba.data[srcIndex + 2]
269
- data[dstIndex + 3] = rgba.data[srcIndex + 3]
270
- }
271
- }
272
-
273
- return { width, height, data }
274
- }
package/worker/util.js DELETED
@@ -1,42 +0,0 @@
1
- import fs from 'bare-fs'
2
- import fetch from 'bare-fetch'
3
- import getMimeType from 'get-mime-type'
4
- import getFileFormat from 'get-file-format'
5
-
6
- export const log = (...args) => console.log('[bare-media]', ...args)
7
-
8
- export async function getBuffer({ path, httpLink, buffer }) {
9
- if (buffer) return buffer
10
-
11
- if (path) {
12
- return fs.readFileSync(path)
13
- }
14
-
15
- if (httpLink) {
16
- const response = await fetch(httpLink)
17
- return await response.buffer()
18
- }
19
-
20
- throw new Error(
21
- 'At least one of "path", "httpLink" or "buffer" must be provided'
22
- )
23
- }
24
-
25
- export function detectMimeType(buffer, path) {
26
- return getMimeType(getFileFormat(buffer)) || getMimeType(path)
27
- }
28
-
29
- export function calculateFitDimensions(width, height, maxWidth, maxHeight) {
30
- if (width <= maxWidth && height <= maxHeight) {
31
- return { width, height }
32
- }
33
-
34
- const widthRatio = maxWidth / width
35
- const heightRatio = maxHeight / height
36
- const ratio = Math.min(widthRatio, heightRatio)
37
-
38
- return {
39
- width: Math.round(width * ratio),
40
- height: Math.round(height * ratio)
41
- }
42
- }